///|
/// Why a diagnostic was not emitted by a policy evaluation.
pub(all) enum SuppressionReason {
BelowMinimum
CodeAllowed(String)
NotSelected(String)
Duplicate
SeverityBudget(Severity, Int)
TotalBudget(Int)
} derive(Eq, Debug)
///|
pub fn SuppressionReason::name(self : SuppressionReason) -> String {
match self {
BelowMinimum => "below-minimum"
CodeAllowed(_) => "code-allowed"
NotSelected(_) => "not-selected"
Duplicate => "duplicate"
SeverityBudget(_, _) => "severity-budget"
TotalBudget(_) => "total-budget"
}
}
///|
pub fn SuppressionReason::message(self : SuppressionReason) -> String {
match self {
BelowMinimum => "diagnostic is below the configured minimum severity"
CodeAllowed(code) => "diagnostic code " + code + " is explicitly allowed"
NotSelected(code) => "diagnostic code " + code + " is not selected"
Duplicate => "an equivalent diagnostic was already emitted"
SeverityBudget(severity, limit) =>
severity.name() +
" diagnostic budget of " +
limit.to_string() +
" was exhausted"
TotalBudget(limit) =>
"total diagnostic budget of " + limit.to_string() + " was exhausted"
}
}
///|
/// A diagnostic together with the reason it was suppressed.
pub struct SuppressedDiagnostic {
diagnostic : Diagnostic
reason : SuppressionReason
}
///|
pub fn SuppressedDiagnostic::diagnostic(
self : SuppressedDiagnostic,
) -> Diagnostic {
self.diagnostic
}
///|
pub fn SuppressedDiagnostic::reason(
self : SuppressedDiagnostic,
) -> SuppressionReason {
self.reason
}
///|
/// Statistics produced while applying a diagnostic policy.
pub struct PolicySummary {
input : Int
emitted : Int
promoted : Int
suppressed : Int
duplicates : Int
budgeted : Int
}
///|
pub fn PolicySummary::input(self : PolicySummary) -> Int {
self.input
}
///|
pub fn PolicySummary::emitted(self : PolicySummary) -> Int {
self.emitted
}
///|
pub fn PolicySummary::promoted(self : PolicySummary) -> Int {
self.promoted
}
///|
pub fn PolicySummary::suppressed(self : PolicySummary) -> Int {
self.suppressed
}
///|
pub fn PolicySummary::duplicates(self : PolicySummary) -> Int {
self.duplicates
}
///|
pub fn PolicySummary::budgeted(self : PolicySummary) -> Int {
self.budgeted
}
///|
/// Result of applying a policy without mutating the input bag.
pub struct PolicyResult {
emitted : DiagnosticBag
suppressed : Array[SuppressedDiagnostic]
summary : PolicySummary
}
///|
pub fn PolicyResult::emitted(self : PolicyResult) -> DiagnosticBag {
DiagnosticBag::from_array(self.emitted.to_array())
}
///|
pub fn PolicyResult::suppressed(
self : PolicyResult,
) -> Array[SuppressedDiagnostic] {
self.suppressed.copy()
}
///|
pub fn PolicyResult::summary(self : PolicyResult) -> PolicySummary {
self.summary
}
///|
pub fn PolicyResult::has_suppressed(self : PolicyResult) -> Bool {
!self.suppressed.is_empty()
}
///|
/// Filtering, promotion, de-duplication, and output-budget rules.
///
/// Rules are evaluated in this order:
/// minimum severity, allow-list suppression, selection, promotion,
/// de-duplication, per-severity budget, then total budget.
pub struct DiagnosticPolicy {
minimum : Severity
allowed_codes : Array[String]
selected_codes : Array[String]
denied_codes : Array[String]
warnings_as_errors : Bool
deduplicate : Bool
advice_limit : Int?
warning_limit : Int?
error_limit : Int?
total_limit : Int?
}
///|
pub fn DiagnosticPolicy::new() -> DiagnosticPolicy {
{
minimum: Advice,
allowed_codes: [],
selected_codes: [],
denied_codes: [],
warnings_as_errors: false,
deduplicate: true,
advice_limit: None,
warning_limit: None,
error_limit: None,
total_limit: None,
}
}
///|
pub fn DiagnosticPolicy::with_minimum(
self : DiagnosticPolicy,
minimum : Severity,
) -> DiagnosticPolicy {
{ ..self, minimum, }
}
///|
pub fn DiagnosticPolicy::allow_code(
self : DiagnosticPolicy,
code : String,
) -> DiagnosticPolicy {
if self.allowed_codes.contains(code) {
self
} else {
{ ..self, allowed_codes: self.allowed_codes + [code] }
}
}
///|
pub fn DiagnosticPolicy::select_code(
self : DiagnosticPolicy,
code : String,
) -> DiagnosticPolicy {
if self.selected_codes.contains(code) {
self
} else {
{ ..self, selected_codes: self.selected_codes + [code] }
}
}
///|
pub fn DiagnosticPolicy::deny_code(
self : DiagnosticPolicy,
code : String,
) -> DiagnosticPolicy {
if self.denied_codes.contains(code) {
self
} else {
{ ..self, denied_codes: self.denied_codes + [code] }
}
}
///|
pub fn DiagnosticPolicy::with_warnings_as_errors(
self : DiagnosticPolicy,
enabled : Bool,
) -> DiagnosticPolicy {
{ ..self, warnings_as_errors: enabled }
}
///|
pub fn DiagnosticPolicy::with_deduplication(
self : DiagnosticPolicy,
enabled : Bool,
) -> DiagnosticPolicy {
{ ..self, deduplicate: enabled }
}
///|
pub fn DiagnosticPolicy::with_advice_limit(
self : DiagnosticPolicy,
limit : Int,
) -> DiagnosticPolicy {
{ ..self, advice_limit: Some(limit.clamp(min=0, max=limit)) }
}
///|
pub fn DiagnosticPolicy::with_warning_limit(
self : DiagnosticPolicy,
limit : Int,
) -> DiagnosticPolicy {
{ ..self, warning_limit: Some(limit.clamp(min=0, max=limit)) }
}
///|
pub fn DiagnosticPolicy::with_error_limit(
self : DiagnosticPolicy,
limit : Int,
) -> DiagnosticPolicy {
{ ..self, error_limit: Some(limit.clamp(min=0, max=limit)) }
}
///|
pub fn DiagnosticPolicy::with_total_limit(
self : DiagnosticPolicy,
limit : Int,
) -> DiagnosticPolicy {
{ ..self, total_limit: Some(limit.clamp(min=0, max=limit)) }
}
///|
fn non_negative(value : Int) -> Int {
if value < 0 {
0
} else {
value
}
}
///|
pub fn DiagnosticPolicy::limits(
self : DiagnosticPolicy,
advice? : Int? = None,
warnings? : Int? = None,
errors? : Int? = None,
total? : Int? = None,
) -> DiagnosticPolicy {
{
..self,
advice_limit: advice.map(non_negative),
warning_limit: warnings.map(non_negative),
error_limit: errors.map(non_negative),
total_limit: total.map(non_negative),
}
}
///|
pub fn DiagnosticPolicy::minimum(self : DiagnosticPolicy) -> Severity {
self.minimum
}
///|
pub fn DiagnosticPolicy::warnings_as_errors(self : DiagnosticPolicy) -> Bool {
self.warnings_as_errors
}
///|
pub fn DiagnosticPolicy::deduplicates(self : DiagnosticPolicy) -> Bool {
self.deduplicate
}
///|
fn diagnostic_code_or_empty(diagnostic : Diagnostic) -> String {
diagnostic.code().unwrap_or("")
}
///|
fn DiagnosticPolicy::early_suppression(
self : DiagnosticPolicy,
diagnostic : Diagnostic,
) -> SuppressionReason? {
if diagnostic.severity().rank() < self.minimum.rank() {
return Some(BelowMinimum)
}
let code = diagnostic_code_or_empty(diagnostic)
if code.length() > 0 && self.allowed_codes.contains(code) {
return Some(CodeAllowed(code))
}
if !self.selected_codes.is_empty() &&
(code.length() == 0 || !self.selected_codes.contains(code)) {
return Some(NotSelected(code))
}
None
}
///|
fn DiagnosticPolicy::promote(
self : DiagnosticPolicy,
diagnostic : Diagnostic,
) -> (Diagnostic, Bool) {
let code = diagnostic_code_or_empty(diagnostic)
let denied = code.length() > 0 && self.denied_codes.contains(code)
let all_warnings = self.warnings_as_errors && diagnostic.severity() == Warning
if diagnostic.severity() != Error && (denied || all_warnings) {
(diagnostic.with_severity(Error), true)
} else {
(diagnostic, false)
}
}
///|
fn write_fingerprint_label(output : StringBuilder, label : Label) -> Unit {
output.write_string(label.source().value().to_string())
output.write_char(':')
output.write_string(label.span().start().to_string())
output.write_char(':')
output.write_string(label.span().end().to_string())
output.write_char(':')
output.write_string(if label.is_primary() { "p" } else { "s" })
output.write_char(':')
output.write_string(label.message())
output.write_char('|')
}
///|
/// Stable identity used by policy de-duplication.
///
/// Notes and help are intentionally excluded: producers often attach contextual
/// notes at different layers while referring to the same underlying problem.
pub fn diagnostic_fingerprint(diagnostic : Diagnostic) -> String {
let output = StringBuilder::new()
output.write_string(diagnostic.severity().name())
output.write_char('|')
output.write_string(diagnostic_code_or_empty(diagnostic))
output.write_char('|')
output.write_string(diagnostic.message())
output.write_char('|')
let labels = diagnostic.labels()
labels.sort_by(fn(left, right) {
let source_order = left.source().compare(right.source())
if source_order != 0 {
source_order
} else {
left.span().start().compare(right.span().start())
}
})
labels.each(fn(label) { write_fingerprint_label(output, label) })
output.to_string()
}
///|
fn limit_for(policy : DiagnosticPolicy, severity : Severity) -> Int? {
match severity {
Advice => policy.advice_limit
Warning => policy.warning_limit
Error => policy.error_limit
}
}
///|
fn count_for(
severity : Severity,
advice : Int,
warnings : Int,
errors : Int,
) -> Int {
match severity {
Advice => advice
Warning => warnings
Error => errors
}
}
///|
pub fn DiagnosticPolicy::apply(
self : DiagnosticPolicy,
bag : DiagnosticBag,
) -> PolicyResult {
let emitted = DiagnosticBag::new()
let suppressed : Array[SuppressedDiagnostic] = []
let fingerprints : Array[String] = []
let mut promoted_count = 0
let mut duplicate_count = 0
let mut budget_count = 0
let mut advice_count = 0
let mut warning_count = 0
let mut error_count = 0
for diagnostic in bag.to_array() {
match self.early_suppression(diagnostic) {
Some(reason) => {
suppressed.push({ diagnostic, reason })
continue
}
None => ()
}
let (candidate, promoted) = self.promote(diagnostic)
let fingerprint = diagnostic_fingerprint(candidate)
if self.deduplicate && fingerprints.contains(fingerprint) {
suppressed.push({ diagnostic: candidate, reason: Duplicate })
duplicate_count += 1
continue
}
let severity = candidate.severity()
let severity_count = count_for(
severity, advice_count, warning_count, error_count,
)
match limit_for(self, severity) {
Some(limit) =>
if severity_count >= limit {
suppressed.push({
diagnostic: candidate,
reason: SeverityBudget(severity, limit),
})
budget_count += 1
continue
}
None => ()
}
match self.total_limit {
Some(limit) =>
if emitted.length() >= limit {
suppressed.push({ diagnostic: candidate, reason: TotalBudget(limit) })
budget_count += 1
continue
}
None => ()
}
fingerprints.push(fingerprint)
emitted.add(candidate)
if promoted {
promoted_count += 1
}
match severity {
Advice => advice_count += 1
Warning => warning_count += 1
Error => error_count += 1
}
}
let summary = {
input: bag.length(),
emitted: emitted.length(),
promoted: promoted_count,
suppressed: suppressed.length(),
duplicates: duplicate_count,
budgeted: budget_count,
}
{ emitted, suppressed, summary }
}