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  }