github.com/kevinklinger/open_terraform@v1.3.6/noninternal/tfdiags/diagnostics.go (about)

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