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  }