Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public struct Diagnostic {
///
/// `org.swift.docc.SummaryContainsLink`
public var identifier: String

/// A unique string that identifies a group of diagnostics whose severity can be controlled by passing `--Werror` and `--Wwarning` flags to `docc`.
public var groupIdentifier: String?

/// A brief summary that describe the problem or issue.
public var summary: String
Expand All @@ -47,6 +50,7 @@ public struct Diagnostic {
severity: DiagnosticSeverity,
range: SourceRange? = nil,
identifier: String,
groupIdentifier: String? = nil,
summary: String,
explanation: String? = nil,
notes: [DiagnosticNote] = []
Expand All @@ -55,6 +59,7 @@ public struct Diagnostic {
self.severity = severity
self.range = range
self.identifier = identifier
self.groupIdentifier = groupIdentifier
self.summary = summary
self.explanation = explanation
self.notes = notes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ public final class DiagnosticEngine {

/// Determines whether warnings will be treated as errors.
private let treatWarningsAsErrors: Bool

/// A list of diagnostic identifiers that are explicitly lowered to a "warning" severity.
package var diagnosticIDsWithWarningSeverity: Set<String>
/// A list of diagnostic identifiers that are explicitly raised to an "error" severity.
package var diagnosticIDsWithErrorSeverity: Set<String>

/// Determines which problems should be emitted.
private func shouldEmit(_ problem: Problem) -> Bool {
problem.diagnostic.severity.rawValue <= filterLevel.rawValue
Expand All @@ -52,9 +56,21 @@ public final class DiagnosticEngine {
}

/// Creates a new diagnostic engine instance with no consumers.
public init(filterLevel: DiagnosticSeverity = .warning, treatWarningsAsErrors: Bool = false) {
/// - Parameters:
/// - filterLevel: The lowers severity (inclusive) that the engine emits to its consumers.
/// - treatWarningsAsErrors: A Boolean value indicating whether the engine raises the severity of warnings to "error" (unless `warningGroupsWithWarningSeverity` explicitly lowers the severity of that diagnostic to a warning)
/// - diagnosticIDsWithWarningSeverity: A list of diagnostic identifiers that are explicitly lowered to a "warning" severity.
/// - diagnosticIDsWithErrorSeverity: A list of diagnostic identifiers that are explicitly raised to an "error" severity.
public init(
filterLevel: DiagnosticSeverity = .warning,
treatWarningsAsErrors: Bool = false,
diagnosticIDsWithWarningSeverity: Set<String> = [],
diagnosticIDsWithErrorSeverity: Set<String> = []
) {
self.filterLevel = filterLevel
self.treatWarningsAsErrors = treatWarningsAsErrors
self.diagnosticIDsWithWarningSeverity = diagnosticIDsWithWarningSeverity
self.diagnosticIDsWithErrorSeverity = diagnosticIDsWithErrorSeverity
}

/// Removes all of the encountered diagnostics from this engine.
Expand All @@ -77,9 +93,7 @@ public final class DiagnosticEngine {
public func emit(_ problems: [Problem]) {
let mappedProblems = problems.map { problem -> Problem in
var problem = problem
if treatWarningsAsErrors, problem.diagnostic.severity == .warning {
problem.diagnostic.severity = .error
}
updateDiagnosticSeverity(&problem.diagnostic)
return problem
}
let filteredProblems = mappedProblems.filter(shouldEmit)
Expand Down Expand Up @@ -125,4 +139,18 @@ public final class DiagnosticEngine {
$0.removeValue(forKey: ObjectIdentifier(consumer))
}
}

private func updateDiagnosticSeverity(_ diagnostic: inout Diagnostic) {
func _severity(identifier: String) -> DiagnosticSeverity? {
if diagnosticIDsWithErrorSeverity.contains(identifier) { .error }
else if diagnosticIDsWithWarningSeverity.contains(identifier) { .warning }
else { nil }
}

if let severity = diagnostic.groupIdentifier.flatMap(_severity) ?? _severity(identifier: diagnostic.identifier) {
diagnostic.severity = severity
} else if treatWarningsAsErrors, diagnostic.severity == .warning {
diagnostic.severity = .error
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public struct ConvertAction: AsyncAction {
/// - formatConsoleOutputForTools: `true` if the convert action should write diagnostics to the console in a format suitable for parsing by an IDE or other tool, otherwise `false`.
/// - inheritDocs: `true` if the convert action should retain the original documentation content for inherited symbols, otherwise `false`.
/// - treatWarningsAsErrors: `true` if the convert action should treat warnings as errors, otherwise `false`.
/// - diagnosticIDsWithWarningSeverity: A list of diagnostic identifiers that are explicitly lowered to a "warning" severity.
/// - diagnosticIDsWithErrorSeverity: A list of diagnostic identifiers that are explicitly raised to an "error" severity.
/// - experimentalEnableCustomTemplates: `true` if the convert action should enable support for custom "header.html" and "footer.html" template files, otherwise `false`.
/// - experimentalModifyCatalogWithGeneratedCuration: `true` if the convert action should write documentation extension files containing markdown representations of DocC's automatic curation into the `documentationBundleURL`, otherwise `false`.
/// - transformForStaticHosting: `true` if the convert action should process the build documentation archive so that it supports a static hosting environment, otherwise `false`.
Expand Down Expand Up @@ -88,6 +90,8 @@ public struct ConvertAction: AsyncAction {
formatConsoleOutputForTools: Bool = false,
inheritDocs: Bool = false,
treatWarningsAsErrors: Bool = false,
diagnosticIDsWithWarningSeverity: Set<String> = [],
diagnosticIDsWithErrorSeverity: Set<String> = [],
experimentalEnableCustomTemplates: Bool = false,
experimentalModifyCatalogWithGeneratedCuration: Bool = false,
transformForStaticHosting: Bool = false,
Expand Down Expand Up @@ -132,7 +136,10 @@ public struct ConvertAction: AsyncAction {
self.experimentalModifyCatalogWithGeneratedCuration = experimentalModifyCatalogWithGeneratedCuration

let engine = diagnosticEngine ?? DiagnosticEngine(treatWarningsAsErrors: treatWarningsAsErrors)
// Set these properties even if the caller passed a base diagnostic engine
engine.filterLevel = filterLevel
engine.diagnosticIDsWithWarningSeverity = diagnosticIDsWithWarningSeverity
engine.diagnosticIDsWithErrorSeverity = diagnosticIDsWithErrorSeverity
if let diagnosticFilePath {
engine.add(DiagnosticFileWriter(outputPath: diagnosticFilePath))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ extension ConvertAction {
formatConsoleOutputForTools: convert.formatConsoleOutputForTools,
inheritDocs: convert.enableInheritedDocs,
treatWarningsAsErrors: convert.warningsAsErrors,
diagnosticIDsWithWarningSeverity: Set(convert.diagnosticIDsWithWarningSeverity),
diagnosticIDsWithErrorSeverity: Set(convert.diagnosticIDsWithErrorSeverity),
experimentalEnableCustomTemplates: convert.experimentalEnableCustomTemplates,
experimentalModifyCatalogWithGeneratedCuration: convert.experimentalModifyCatalogWithGeneratedCuration,
transformForStaticHosting: convert.transformForStaticHosting,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,30 @@ extension Docc {
help: "Format output to the console intended for an IDE or other tool to parse.")
var formatConsoleOutputForTools = false

@Flag(help: "Treat warnings as errors")
@Flag(help: "Treat all warnings as errors")
var warningsAsErrors = false

@Option(
name: [.customLong("Werror")], // This matches Swift's spellings
parsing: ArrayParsingStrategy.singleValue,
help: ArgumentHelp("Treat this diagnostic group as an error", valueName: "diagnostic-id")
)
var warningGroupsWithErrorSeverity: [String] = []

@Option(
name: [.customLong("Wwarning")], // This matches Swift's spellings
parsing: ArrayParsingStrategy.singleValue,
help: ArgumentHelp(
"Treat this diagnostic group as a warning",
discussion: """
If you pass '--warnings-as-errors' you can use this flag to lower one or more specific groups of diagnostics to a warnings severity.
""",
valueName: "diagnostic-id"
)
)
var warningGroupsWithWarningSeverity: [String] = []

func validate() throws {
mutating func validate() throws {
if analyze && diagnosticLevel != nil {
warnAboutDiagnostic(.init(
severity: .information,
Expand All @@ -260,6 +280,26 @@ extension Docc {
"""
))
}

if !warningGroupsWithErrorSeverity.isEmpty,
!warningGroupsWithWarningSeverity.isEmpty
{
// Check if there's overlap between the two diagnostic levels
let diagnosticIDsWithConflictingSeverities = Set(warningGroupsWithErrorSeverity).intersection(warningGroupsWithWarningSeverity)
if !diagnosticIDsWithConflictingSeverities.isEmpty {
for diagnosticID in diagnosticIDsWithConflictingSeverities.sorted() {
warnAboutDiagnostic(.init(
severity: .information,
identifier: "org.swift.docc.ConflictingDiagnosticSeverity",
summary: "Conflicting severity (both '--Wwarning' and '--Werror') for diagnostic group '\(diagnosticID)'."
))
}

// Because we don't know which severity was specified last, remove the diagnostic ID from both groups
warningGroupsWithErrorSeverity.removeAll(where: { diagnosticIDsWithConflictingSeverities.contains($0) })
warningGroupsWithWarningSeverity.removeAll(where: { diagnosticIDsWithConflictingSeverities.contains($0) })
}
}
}

private static let supportedDiagnosticLevelsMessage = """
Expand All @@ -276,7 +316,19 @@ extension Docc {
get { diagnosticOptions.warningsAsErrors }
set { diagnosticOptions.warningsAsErrors = newValue }
}


/// A list of diagnostic identifiers that are explicitly lowered to a "warning" severity.
public var diagnosticIDsWithWarningSeverity: [String] {
get { diagnosticOptions.warningGroupsWithWarningSeverity }
set { diagnosticOptions.warningGroupsWithWarningSeverity = newValue }
}

/// A list of diagnostic identifiers that are explicitly raised to an "error" severity.
public var diagnosticIDsWithErrorSeverity: [String] {
get { diagnosticOptions.warningGroupsWithErrorSeverity }
set { diagnosticOptions.warningGroupsWithErrorSeverity = newValue }
}

/// A user-provided value that is true if output to the console should be formatted for an IDE or other tool to parse.
public var formatConsoleOutputForTools: Bool {
get { diagnosticOptions.formatConsoleOutputForTools }
Expand Down
108 changes: 106 additions & 2 deletions Tests/SwiftDocCTests/Diagnostics/DiagnosticEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class DiagnosticEngineTests: XCTestCase {
let diagnostic = Diagnostic(source: nil, severity: .error, range: nil, identifier: "org.swift.docc.test", summary: "Test diagnostic")
let problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
let engine = DiagnosticEngine()
let exp = expectation(description: "Recieved diagnostic")
let exp = expectation(description: "Received diagnostic")
let consumer = TestConsumer(exp)

XCTAssertEqual(engine.problems.count, 0)
Expand All @@ -52,7 +52,7 @@ class DiagnosticEngineTests: XCTestCase {
let diagnostic = Diagnostic(source: nil, severity: .error, range: nil, identifier: "org.swift.docc.test", summary: "Test diagnostic")
let problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
let engine = DiagnosticEngine()
let exp = expectation(description: "Recieved diagnostic")
let exp = expectation(description: "Received diagnostic")
exp.expectedFulfillmentCount = 2
let consumerA = TestConsumer(exp)
let consumerB = TestConsumer(exp)
Expand Down Expand Up @@ -164,4 +164,108 @@ class DiagnosticEngineTests: XCTestCase {
error: Test warning
""")
}

func testRaiseSeverityOfSpecificDiagnostics() {
let warnings = ["One", "Two", "Three"].map { id in
Problem(diagnostic: Diagnostic(source: nil, severity: .warning, range: nil, identifier: id, summary: "Test diagnostic \(id.lowercased())"), possibleSolutions: [])
}

let defaultEngine = DiagnosticEngine()
defaultEngine.emit(warnings)

XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: defaultEngine.problems, options: .formatConsoleOutputForTools), """
warning: Test diagnostic one
warning: Test diagnostic two
warning: Test diagnostic three
""")

let engineWithSpecificDiagnosticsRaised = DiagnosticEngine(diagnosticIDsWithErrorSeverity: ["Two", "Unknown"])
engineWithSpecificDiagnosticsRaised.emit(warnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithSpecificDiagnosticsRaised.problems, options: .formatConsoleOutputForTools), """
warning: Test diagnostic one
error: Test diagnostic two
warning: Test diagnostic three
""")

let engineWithFilterAndSpecificDiagnosticsRaised = DiagnosticEngine(filterLevel: .error, diagnosticIDsWithErrorSeverity: ["Two", "Unknown"])
engineWithFilterAndSpecificDiagnosticsRaised.emit(warnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithFilterAndSpecificDiagnosticsRaised.problems, options: .formatConsoleOutputForTools), """
error: Test diagnostic two
""")
}

func testLowerSeverityOfSpecificDiagnostics() {
let warnings = ["One", "Two", "Three"].map { id in
Problem(diagnostic: Diagnostic(source: nil, severity: .warning, range: nil, identifier: id, summary: "Test diagnostic \(id.lowercased())"), possibleSolutions: [])
}

let engineWithRaisedSeverity = DiagnosticEngine(treatWarningsAsErrors: true)
engineWithRaisedSeverity.emit(warnings)

XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithRaisedSeverity.problems, options: .formatConsoleOutputForTools), """
error: Test diagnostic one
error: Test diagnostic two
error: Test diagnostic three
""")

let engineWithSpecificDiagnosticsLowered = DiagnosticEngine(treatWarningsAsErrors: true, diagnosticIDsWithWarningSeverity: ["Two", "Unknown"])
engineWithSpecificDiagnosticsLowered.emit(warnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithSpecificDiagnosticsLowered.problems, options: .formatConsoleOutputForTools), """
error: Test diagnostic one
warning: Test diagnostic two
error: Test diagnostic three
""")

let engineWithFilterAndSpecificDiagnosticsLowered = DiagnosticEngine(filterLevel: .error, treatWarningsAsErrors: true, diagnosticIDsWithWarningSeverity: ["Two", "Unknown"])
engineWithFilterAndSpecificDiagnosticsLowered.emit(warnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithFilterAndSpecificDiagnosticsLowered.problems, options: .formatConsoleOutputForTools), """
error: Test diagnostic one
error: Test diagnostic three
""")
}

func testRaiseSeverityOfDiagnosticGroups() {
let letterWarnings = ["A", "B", "C"].map { id in
Problem(diagnostic: Diagnostic(source: nil, severity: .warning, range: nil, identifier: id, groupIdentifier: "Letter", summary: "Test diagnostic \(id)"), possibleSolutions: [])
}
let numberWarnings = ["1", "2", "3"].map { id in
Problem(diagnostic: Diagnostic(source: nil, severity: .warning, range: nil, identifier: id, groupIdentifier: "Number", summary: "Test diagnostic \(id)"), possibleSolutions: [])
}

let engineWithRaisedLetterSeverity = DiagnosticEngine(diagnosticIDsWithErrorSeverity: ["Letter"])
engineWithRaisedLetterSeverity.emit(letterWarnings)
engineWithRaisedLetterSeverity.emit(numberWarnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithRaisedLetterSeverity.problems, options: .formatConsoleOutputForTools), """
error: Test diagnostic A
error: Test diagnostic B
error: Test diagnostic C
warning: Test diagnostic 1
warning: Test diagnostic 2
warning: Test diagnostic 3
""")

let engineWithRaisedNumberSeverity = DiagnosticEngine(diagnosticIDsWithErrorSeverity: ["Number"])
engineWithRaisedNumberSeverity.emit(letterWarnings)
engineWithRaisedNumberSeverity.emit(numberWarnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithRaisedNumberSeverity.problems, options: .formatConsoleOutputForTools), """
warning: Test diagnostic A
warning: Test diagnostic B
warning: Test diagnostic C
error: Test diagnostic 1
error: Test diagnostic 2
error: Test diagnostic 3
""")

let engineWithRaisedNumberSeverityAndOneLetter = DiagnosticEngine(diagnosticIDsWithErrorSeverity: ["Number", "B"])
engineWithRaisedNumberSeverityAndOneLetter.emit(letterWarnings)
engineWithRaisedNumberSeverityAndOneLetter.emit(numberWarnings)
XCTAssertEqual(DiagnosticConsoleWriter.formattedDescription(for: engineWithRaisedNumberSeverityAndOneLetter.problems, options: .formatConsoleOutputForTools), """
warning: Test diagnostic A
error: Test diagnostic B
warning: Test diagnostic C
error: Test diagnostic 1
error: Test diagnostic 2
error: Test diagnostic 3
""")
}
}
Loading