github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/diagnostics.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 (
     9  	"bytes"
    10  	"fmt"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/hashicorp/errwrap"
    16  	multierror "github.com/hashicorp/go-multierror"
    17  	"github.com/hashicorp/hcl/v2"
    18  )
    19  
    20  // Diagnostics is a list of diagnostics. Diagnostics is intended to be used
    21  // where a Go "error" might normally be used, allowing richer information
    22  // to be conveyed (more context, support for warnings).
    23  //
    24  // A nil Diagnostics is a valid, empty diagnostics list, thus allowing
    25  // heap allocation to be avoided in the common case where there are no
    26  // diagnostics to report at all.
    27  type Diagnostics []Diagnostic
    28  
    29  // Append is the main interface for constructing Diagnostics lists, taking
    30  // an existing list (which may be nil) and appending the new objects to it
    31  // after normalizing them to be implementations of Diagnostic.
    32  //
    33  // The usual pattern for a function that natively "speaks" diagnostics is:
    34  //
    35  //	// Create a nil Diagnostics at the start of the function
    36  //	var diags diag.Diagnostics
    37  //
    38  //	// At later points, build on it if errors / warnings occur:
    39  //	foo, err := DoSomethingRisky()
    40  //	if err != nil {
    41  //	    diags = diags.Append(err)
    42  //	}
    43  //
    44  //	// Eventually return the result and diagnostics in place of error
    45  //	return result, diags
    46  //
    47  // Append accepts a variety of different diagnostic-like types, including
    48  // native Go errors and HCL diagnostics. It also knows how to unwrap
    49  // a multierror.Error into separate error diagnostics. It can be passed
    50  // another Diagnostics to concatenate the two lists. If given something
    51  // it cannot handle, this function will panic.
    52  func (diags Diagnostics) Append(new ...interface{}) Diagnostics {
    53  	for _, item := range new {
    54  		if item == nil {
    55  			continue
    56  		}
    57  
    58  		switch ti := item.(type) {
    59  		case Diagnostic:
    60  			diags = append(diags, ti)
    61  		case Diagnostics:
    62  			diags = append(diags, ti...) // flatten
    63  		case diagnosticsAsError:
    64  			diags = diags.Append(ti.Diagnostics) // unwrap
    65  		case NonFatalError:
    66  			diags = diags.Append(ti.Diagnostics) // unwrap
    67  		case hcl.Diagnostics:
    68  			for _, hclDiag := range ti {
    69  				diags = append(diags, hclDiagnostic{hclDiag})
    70  			}
    71  		case *hcl.Diagnostic:
    72  			diags = append(diags, hclDiagnostic{ti})
    73  		case *multierror.Error:
    74  			for _, err := range ti.Errors {
    75  				diags = append(diags, nativeError{err})
    76  			}
    77  		case error:
    78  			switch {
    79  			case errwrap.ContainsType(ti, Diagnostics(nil)):
    80  				// If we have an errwrap wrapper with a Diagnostics hiding
    81  				// inside then we'll unpick it here to get access to the
    82  				// individual diagnostics.
    83  				diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil)))
    84  			case errwrap.ContainsType(ti, hcl.Diagnostics(nil)):
    85  				// Likewise, if we have HCL diagnostics we'll unpick that too.
    86  				diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil)))
    87  			default:
    88  				diags = append(diags, nativeError{ti})
    89  			}
    90  		default:
    91  			panic(fmt.Errorf("can't construct diagnostic(s) from %T", item))
    92  		}
    93  	}
    94  
    95  	// Given the above, we should never end up with a non-nil empty slice
    96  	// here, but we'll make sure of that so callers can rely on empty == nil
    97  	if len(diags) == 0 {
    98  		return nil
    99  	}
   100  
   101  	return diags
   102  }
   103  
   104  // HasErrors returns true if any of the diagnostics in the list have
   105  // a severity of Error.
   106  func (diags Diagnostics) HasErrors() bool {
   107  	for _, diag := range diags {
   108  		if diag.Severity() == Error {
   109  			return true
   110  		}
   111  	}
   112  	return false
   113  }
   114  
   115  // ForRPC returns a version of the receiver that has been simplified so that
   116  // it is friendly to RPC protocols.
   117  //
   118  // Currently this means that it can be serialized with encoding/gob and
   119  // subsequently re-inflated. It may later grow to include other serialization
   120  // formats.
   121  //
   122  // Note that this loses information about the original objects used to
   123  // construct the diagnostics, so e.g. the errwrap API will not work as
   124  // expected on an error-wrapped Diagnostics that came from ForRPC.
   125  func (diags Diagnostics) ForRPC() Diagnostics {
   126  	ret := make(Diagnostics, len(diags))
   127  	for i := range diags {
   128  		ret[i] = makeRPCFriendlyDiag(diags[i])
   129  	}
   130  	return ret
   131  }
   132  
   133  // Err flattens a diagnostics list into a single Go error, or to nil
   134  // if the diagnostics list does not include any error-level diagnostics.
   135  //
   136  // This can be used to smuggle diagnostics through an API that deals in
   137  // native errors, but unfortunately it will lose any warnings that aren't
   138  // accompanied by at least one error since such APIs have no mechanism through
   139  // which to report those.
   140  //
   141  //	return result, diags.Error()
   142  func (diags Diagnostics) Err() error {
   143  	if !diags.HasErrors() {
   144  		return nil
   145  	}
   146  	return diagnosticsAsError{diags}
   147  }
   148  
   149  // ErrWithWarnings is similar to Err except that it will also return a non-nil
   150  // error if the receiver contains only warnings.
   151  //
   152  // In the warnings-only situation, the result is guaranteed to be of dynamic
   153  // type NonFatalError, allowing diagnostics-aware callers to type-assert
   154  // and unwrap it, treating it as non-fatal.
   155  //
   156  // This should be used only in contexts where the caller is able to recognize
   157  // and handle NonFatalError. For normal callers that expect a lack of errors
   158  // to be signaled by nil, use just Diagnostics.Err.
   159  func (diags Diagnostics) ErrWithWarnings() error {
   160  	if len(diags) == 0 {
   161  		return nil
   162  	}
   163  	if diags.HasErrors() {
   164  		return diags.Err()
   165  	}
   166  	return NonFatalError{diags}
   167  }
   168  
   169  // NonFatalErr is similar to Err except that it always returns either nil
   170  // (if there are no diagnostics at all) or NonFatalError.
   171  //
   172  // This allows diagnostics to be returned over an error return channel while
   173  // being explicit that the diagnostics should not halt processing.
   174  //
   175  // This should be used only in contexts where the caller is able to recognize
   176  // and handle NonFatalError. For normal callers that expect a lack of errors
   177  // to be signaled by nil, use just Diagnostics.Err.
   178  func (diags Diagnostics) NonFatalErr() error {
   179  	if len(diags) == 0 {
   180  		return nil
   181  	}
   182  	return NonFatalError{diags}
   183  }
   184  
   185  // Sort applies an ordering to the diagnostics in the receiver in-place.
   186  //
   187  // The ordering is: warnings before errors, sourceless before sourced,
   188  // short source paths before long source paths, and then ordering by
   189  // position within each file.
   190  //
   191  // Diagnostics that do not differ by any of these sortable characteristics
   192  // will remain in the same relative order after this method returns.
   193  func (diags Diagnostics) Sort() {
   194  	sort.Stable(sortDiagnostics(diags))
   195  }
   196  
   197  type diagnosticsAsError struct {
   198  	Diagnostics
   199  }
   200  
   201  func (dae diagnosticsAsError) Error() string {
   202  	diags := dae.Diagnostics
   203  	switch {
   204  	case len(diags) == 0:
   205  		// should never happen, since we don't create this wrapper if
   206  		// there are no diagnostics in the list.
   207  		return "no errors"
   208  	case len(diags) == 1:
   209  		desc := diags[0].Description()
   210  		if desc.Detail == "" {
   211  			return desc.Summary
   212  		}
   213  		return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
   214  	default:
   215  		var ret bytes.Buffer
   216  		fmt.Fprintf(&ret, "%d problems:\n", len(diags))
   217  		for _, diag := range dae.Diagnostics {
   218  			desc := diag.Description()
   219  			if desc.Detail == "" {
   220  				fmt.Fprintf(&ret, "\n- %s", desc.Summary)
   221  			} else {
   222  				fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
   223  			}
   224  		}
   225  		return ret.String()
   226  	}
   227  }
   228  
   229  // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped
   230  // diagnostics object can be picked apart by errwrap-aware code.
   231  func (dae diagnosticsAsError) WrappedErrors() []error {
   232  	var errs []error
   233  	for _, diag := range dae.Diagnostics {
   234  		if wrapper, isErr := diag.(nativeError); isErr {
   235  			errs = append(errs, wrapper.err)
   236  		}
   237  	}
   238  	return errs
   239  }
   240  
   241  // NonFatalError is a special error type, returned by
   242  // Diagnostics.ErrWithWarnings and Diagnostics.NonFatalErr,
   243  // that indicates that the wrapped diagnostics should be treated as non-fatal.
   244  // Callers can conditionally type-assert an error to this type in order to
   245  // detect the non-fatal scenario and handle it in a different way.
   246  type NonFatalError struct {
   247  	Diagnostics
   248  }
   249  
   250  func (woe NonFatalError) Error() string {
   251  	diags := woe.Diagnostics
   252  	switch {
   253  	case len(diags) == 0:
   254  		// should never happen, since we don't create this wrapper if
   255  		// there are no diagnostics in the list.
   256  		return "no errors or warnings"
   257  	case len(diags) == 1:
   258  		desc := diags[0].Description()
   259  		if desc.Detail == "" {
   260  			return desc.Summary
   261  		}
   262  		return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
   263  	default:
   264  		var ret bytes.Buffer
   265  		if diags.HasErrors() {
   266  			fmt.Fprintf(&ret, "%d problems:\n", len(diags))
   267  		} else {
   268  			fmt.Fprintf(&ret, "%d warnings:\n", len(diags))
   269  		}
   270  		for _, diag := range woe.Diagnostics {
   271  			desc := diag.Description()
   272  			if desc.Detail == "" {
   273  				fmt.Fprintf(&ret, "\n- %s", desc.Summary)
   274  			} else {
   275  				fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
   276  			}
   277  		}
   278  		return ret.String()
   279  	}
   280  }
   281  
   282  // sortDiagnostics is an implementation of sort.Interface
   283  type sortDiagnostics []Diagnostic
   284  
   285  var _ sort.Interface = sortDiagnostics(nil)
   286  
   287  func (sd sortDiagnostics) Len() int {
   288  	return len(sd)
   289  }
   290  
   291  func (sd sortDiagnostics) Less(i, j int) bool {
   292  	iD, jD := sd[i], sd[j]
   293  	iSev, jSev := iD.Severity(), jD.Severity()
   294  	iSrc, jSrc := iD.Source(), jD.Source()
   295  
   296  	switch {
   297  
   298  	case iSev != jSev:
   299  		return iSev == Warning
   300  
   301  	case (iSrc.Subject == nil) != (jSrc.Subject == nil):
   302  		return iSrc.Subject == nil
   303  
   304  	case iSrc.Subject != nil && *iSrc.Subject != *jSrc.Subject:
   305  		iSubj := iSrc.Subject
   306  		jSubj := jSrc.Subject
   307  		switch {
   308  		case iSubj.Filename != jSubj.Filename:
   309  			// Path with fewer segments goes first if they are different lengths
   310  			sep := string(filepath.Separator)
   311  			iCount := strings.Count(iSubj.Filename, sep)
   312  			jCount := strings.Count(jSubj.Filename, sep)
   313  			if iCount != jCount {
   314  				return iCount < jCount
   315  			}
   316  			return iSubj.Filename < jSubj.Filename
   317  		case iSubj.Start.Byte != jSubj.Start.Byte:
   318  			return iSubj.Start.Byte < jSubj.Start.Byte
   319  		case iSubj.End.Byte != jSubj.End.Byte:
   320  			return iSubj.End.Byte < jSubj.End.Byte
   321  		}
   322  		fallthrough
   323  
   324  	default:
   325  		// The remaining properties do not have a defined ordering, so
   326  		// we'll leave it unspecified. Since we use sort.Stable in
   327  		// the caller of this, the ordering of remaining items will
   328  		// be preserved.
   329  		return false
   330  	}
   331  }
   332  
   333  func (sd sortDiagnostics) Swap(i, j int) {
   334  	sd[i], sd[j] = sd[j], sd[i]
   335  }