opentofu/tfdiags/consolidate_warnings.go
Martin Atkins c06675c616 command: New -compact-warnings option
When warnings appear in isolation (not accompanied by an error) it's
reasonable to want to defer resolving them for a while because they are
not actually blocking immediate work.

However, our warning messages tend to be long by default in order to
include all of the necessary context to understand the implications of
the warning, and that can make them overwhelming when combined with other
output.

As a compromise, this adds a new CLI option -compact-warnings which is
supported for all the main operation commands and which uses a more
compact format to print out warnings as long as they aren't also
accompanied by errors.

The default remains unchanged except that the threshold for consolidating
warning messages is reduced to one so that we'll now only show one of
each distinct warning summary.

Full warning messages are always shown if there's at least one error
included in the diagnostic set too, because in that case the warning
message could contain additional context to help understand the error.
2019-12-10 11:53:14 -08:00

147 lines
4.6 KiB
Go

package tfdiags
import "fmt"
// ConsolidateWarnings checks if there is an unreasonable amount of warnings
// with the same summary in the receiver and, if so, returns a new diagnostics
// with some of those warnings consolidated into a single warning in order
// to reduce the verbosity of the output.
//
// This mechanism is here primarily for diagnostics printed out at the CLI. In
// other contexts it is likely better to just return the warnings directly,
// particularly if they are going to be interpreted by software rather than
// by a human reader.
//
// The returned slice always has a separate backing array from the reciever,
// but some diagnostic values themselves might be shared.
//
// The definition of "unreasonable" is given as the threshold argument. At most
// that many warnings with the same summary will be shown.
func (diags Diagnostics) ConsolidateWarnings(threshold int) Diagnostics {
if len(diags) == 0 {
return nil
}
newDiags := make(Diagnostics, 0, len(diags))
// We'll track how many times we've seen each warning summary so we can
// decide when to start consolidating. Once we _have_ started consolidating,
// we'll also track the object representing the consolidated warning
// so we can continue appending to it.
warningStats := make(map[string]int)
warningGroups := make(map[string]*warningGroup)
for _, diag := range diags {
severity := diag.Severity()
if severity != Warning || diag.Source().Subject == nil {
// Only warnings can get special treatment, and we only
// consolidate warnings that have source locations because
// our primary goal here is to deal with the situation where
// some configuration language feature is producing a warning
// each time it's used across a potentially-large config.
newDiags = newDiags.Append(diag)
continue
}
desc := diag.Description()
summary := desc.Summary
if g, ok := warningGroups[summary]; ok {
// We're already grouping this one, so we'll just continue it.
g.Append(diag)
continue
}
warningStats[summary]++
if warningStats[summary] == threshold {
// Initially creating the group doesn't really change anything
// visibly in the result, since a group with only one warning
// is just a passthrough anyway, but once we do this any additional
// warnings with the same summary will get appended to this group.
g := &warningGroup{}
newDiags = newDiags.Append(g)
warningGroups[summary] = g
g.Append(diag)
continue
}
// If this warning is not consolidating yet then we'll just append
// it directly.
newDiags = newDiags.Append(diag)
}
return newDiags
}
// A warningGroup is one or more warning diagnostics grouped together for
// UI consolidation purposes.
//
// A warningGroup with only one diagnostic in it is just a passthrough for
// that one diagnostic. If it has more than one then it will behave mostly
// like the first one but its detail message will include an additional
// sentence mentioning the consolidation. A warningGroup with no diagnostics
// at all is invalid and will panic when used.
type warningGroup struct {
Warnings Diagnostics
}
var _ Diagnostic = (*warningGroup)(nil)
func (wg *warningGroup) Severity() Severity {
return wg.Warnings[0].Severity()
}
func (wg *warningGroup) Description() Description {
desc := wg.Warnings[0].Description()
if len(wg.Warnings) < 2 {
return desc
}
extraCount := len(wg.Warnings) - 1
var msg string
switch extraCount {
case 1:
msg = "(and one more similar warning elsewhere)"
default:
msg = fmt.Sprintf("(and %d more similar warnings elsewhere)", extraCount)
}
if desc.Detail != "" {
desc.Detail = desc.Detail + "\n\n" + msg
} else {
desc.Detail = msg
}
return desc
}
func (wg *warningGroup) Source() Source {
return wg.Warnings[0].Source()
}
func (wg *warningGroup) FromExpr() *FromExpr {
return wg.Warnings[0].FromExpr()
}
func (wg *warningGroup) Append(diag Diagnostic) {
if diag.Severity() != Warning {
panic("can't append a non-warning diagnostic to a warningGroup")
}
wg.Warnings = append(wg.Warnings, diag)
}
// WarningGroupSourceRanges can be used in conjunction with
// Diagnostics.ConsolidateWarnings to recover the full set of original source
// locations from a consolidated warning.
//
// For convenience, this function accepts any diagnostic and will just return
// the single Source value from any diagnostic that isn't a warning group.
func WarningGroupSourceRanges(diag Diagnostic) []Source {
wg, ok := diag.(*warningGroup)
if !ok {
return []Source{diag.Source()}
}
ret := make([]Source, len(wg.Warnings))
for i, wrappedDiag := range wg.Warnings {
ret[i] = wrappedDiag.Source()
}
return ret
}