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