github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/tfdiags/consolidate_warnings.go (about)

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