github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/consolidate_warnings.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package tfdiags 5 6 import "fmt" 7 8 // ConsolidateWarnings checks if there is an unreasonable amount of warnings 9 // with the same summary in the receiver and, if so, returns a new diagnostics 10 // with some of those warnings consolidated into a single warning in order 11 // to reduce the verbosity of the output. 12 // 13 // This mechanism is here primarily for diagnostics printed out at the CLI. In 14 // other contexts it is likely better to just return the warnings directly, 15 // particularly if they are going to be interpreted by software rather than 16 // by a human reader. 17 // 18 // The returned slice always has a separate backing array from the reciever, 19 // but some diagnostic values themselves might be shared. 20 // 21 // The definition of "unreasonable" is given as the threshold argument. At most 22 // that many warnings with the same summary will be shown. 23 func (diags Diagnostics) ConsolidateWarnings(threshold int) Diagnostics { 24 if len(diags) == 0 { 25 return nil 26 } 27 28 newDiags := make(Diagnostics, 0, len(diags)) 29 30 // We'll track how many times we've seen each warning summary so we can 31 // decide when to start consolidating. Once we _have_ started consolidating, 32 // we'll also track the object representing the consolidated warning 33 // so we can continue appending to it. 34 warningStats := make(map[string]int) 35 warningGroups := make(map[string]*warningGroup) 36 37 for _, diag := range diags { 38 severity := diag.Severity() 39 if severity != Warning || diag.Source().Subject == nil { 40 // Only warnings can get special treatment, and we only 41 // consolidate warnings that have source locations because 42 // our primary goal here is to deal with the situation where 43 // some configuration language feature is producing a warning 44 // each time it's used across a potentially-large config. 45 newDiags = newDiags.Append(diag) 46 continue 47 } 48 49 if DoNotConsolidateDiagnostic(diag) { 50 // Then do not consolidate this diagnostic. 51 newDiags = newDiags.Append(diag) 52 continue 53 } 54 55 desc := diag.Description() 56 summary := desc.Summary 57 if g, ok := warningGroups[summary]; ok { 58 // We're already grouping this one, so we'll just continue it. 59 g.Append(diag) 60 continue 61 } 62 63 warningStats[summary]++ 64 if warningStats[summary] == threshold { 65 // Initially creating the group doesn't really change anything 66 // visibly in the result, since a group with only one warning 67 // is just a passthrough anyway, but once we do this any additional 68 // warnings with the same summary will get appended to this group. 69 g := &warningGroup{} 70 newDiags = newDiags.Append(g) 71 warningGroups[summary] = g 72 g.Append(diag) 73 continue 74 } 75 76 // If this warning is not consolidating yet then we'll just append 77 // it directly. 78 newDiags = newDiags.Append(diag) 79 } 80 81 return newDiags 82 } 83 84 // A warningGroup is one or more warning diagnostics grouped together for 85 // UI consolidation purposes. 86 // 87 // A warningGroup with only one diagnostic in it is just a passthrough for 88 // that one diagnostic. If it has more than one then it will behave mostly 89 // like the first one but its detail message will include an additional 90 // sentence mentioning the consolidation. A warningGroup with no diagnostics 91 // at all is invalid and will panic when used. 92 type warningGroup struct { 93 Warnings Diagnostics 94 } 95 96 var _ Diagnostic = (*warningGroup)(nil) 97 98 func (wg *warningGroup) Severity() Severity { 99 return wg.Warnings[0].Severity() 100 } 101 102 func (wg *warningGroup) Description() Description { 103 desc := wg.Warnings[0].Description() 104 if len(wg.Warnings) < 2 { 105 return desc 106 } 107 extraCount := len(wg.Warnings) - 1 108 var msg string 109 switch extraCount { 110 case 1: 111 msg = "(and one more similar warning elsewhere)" 112 default: 113 msg = fmt.Sprintf("(and %d more similar warnings elsewhere)", extraCount) 114 } 115 if desc.Detail != "" { 116 desc.Detail = desc.Detail + "\n\n" + msg 117 } else { 118 desc.Detail = msg 119 } 120 return desc 121 } 122 123 func (wg *warningGroup) Source() Source { 124 return wg.Warnings[0].Source() 125 } 126 127 func (wg *warningGroup) FromExpr() *FromExpr { 128 return wg.Warnings[0].FromExpr() 129 } 130 131 func (wg *warningGroup) ExtraInfo() interface{} { 132 return wg.Warnings[0].ExtraInfo() 133 } 134 135 func (wg *warningGroup) Append(diag Diagnostic) { 136 if diag.Severity() != Warning { 137 panic("can't append a non-warning diagnostic to a warningGroup") 138 } 139 wg.Warnings = append(wg.Warnings, diag) 140 } 141 142 // WarningGroupSourceRanges can be used in conjunction with 143 // Diagnostics.ConsolidateWarnings to recover the full set of original source 144 // locations from a consolidated warning. 145 // 146 // For convenience, this function accepts any diagnostic and will just return 147 // the single Source value from any diagnostic that isn't a warning group. 148 func WarningGroupSourceRanges(diag Diagnostic) []Source { 149 wg, ok := diag.(*warningGroup) 150 if !ok { 151 return []Source{diag.Source()} 152 } 153 154 ret := make([]Source, len(wg.Warnings)) 155 for i, wrappedDiag := range wg.Warnings { 156 ret[i] = wrappedDiag.Source() 157 } 158 return ret 159 }