github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/diagnostics.go (about)

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