| 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) ExtraInfo() interface{} { |
| return wg.Warnings[0].ExtraInfo() |
| } |
| |
| 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 |
| } |