github.com/magodo/terraform@v0.11.12-beta1/tfdiags/diagnostics.go (about)

     1  package tfdiags
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  
     7  	"github.com/hashicorp/errwrap"
     8  	multierror "github.com/hashicorp/go-multierror"
     9  	"github.com/hashicorp/hcl2/hcl"
    10  )
    11  
    12  // Diagnostics is a list of diagnostics. Diagnostics is intended to be used
    13  // where a Go "error" might normally be used, allowing richer information
    14  // to be conveyed (more context, support for warnings).
    15  //
    16  // A nil Diagnostics is a valid, empty diagnostics list, thus allowing
    17  // heap allocation to be avoided in the common case where there are no
    18  // diagnostics to report at all.
    19  type Diagnostics []Diagnostic
    20  
    21  // Append is the main interface for constructing Diagnostics lists, taking
    22  // an existing list (which may be nil) and appending the new objects to it
    23  // after normalizing them to be implementations of Diagnostic.
    24  //
    25  // The usual pattern for a function that natively "speaks" diagnostics is:
    26  //
    27  //     // Create a nil Diagnostics at the start of the function
    28  //     var diags diag.Diagnostics
    29  //
    30  //     // At later points, build on it if errors / warnings occur:
    31  //     foo, err := DoSomethingRisky()
    32  //     if err != nil {
    33  //         diags = diags.Append(err)
    34  //     }
    35  //
    36  //     // Eventually return the result and diagnostics in place of error
    37  //     return result, diags
    38  //
    39  // Append accepts a variety of different diagnostic-like types, including
    40  // native Go errors and HCL diagnostics. It also knows how to unwrap
    41  // a multierror.Error into separate error diagnostics. It can be passed
    42  // another Diagnostics to concatenate the two lists. If given something
    43  // it cannot handle, this function will panic.
    44  func (diags Diagnostics) Append(new ...interface{}) Diagnostics {
    45  	for _, item := range new {
    46  		if item == nil {
    47  			continue
    48  		}
    49  
    50  		switch ti := item.(type) {
    51  		case Diagnostic:
    52  			diags = append(diags, ti)
    53  		case Diagnostics:
    54  			diags = append(diags, ti...) // flatten
    55  		case diagnosticsAsError:
    56  			diags = diags.Append(ti.Diagnostics) // unwrap
    57  		case hcl.Diagnostics:
    58  			for _, hclDiag := range ti {
    59  				diags = append(diags, hclDiagnostic{hclDiag})
    60  			}
    61  		case *hcl.Diagnostic:
    62  			diags = append(diags, hclDiagnostic{ti})
    63  		case *multierror.Error:
    64  			for _, err := range ti.Errors {
    65  				diags = append(diags, nativeError{err})
    66  			}
    67  		case error:
    68  			switch {
    69  			case errwrap.ContainsType(ti, Diagnostics(nil)):
    70  				// If we have an errwrap wrapper with a Diagnostics hiding
    71  				// inside then we'll unpick it here to get access to the
    72  				// individual diagnostics.
    73  				diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil)))
    74  			case errwrap.ContainsType(ti, hcl.Diagnostics(nil)):
    75  				// Likewise, if we have HCL diagnostics we'll unpick that too.
    76  				diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil)))
    77  			default:
    78  				diags = append(diags, nativeError{ti})
    79  			}
    80  		default:
    81  			panic(fmt.Errorf("can't construct diagnostic(s) from %T", item))
    82  		}
    83  	}
    84  
    85  	// Given the above, we should never end up with a non-nil empty slice
    86  	// here, but we'll make sure of that so callers can rely on empty == nil
    87  	if len(diags) == 0 {
    88  		return nil
    89  	}
    90  
    91  	return diags
    92  }
    93  
    94  // HasErrors returns true if any of the diagnostics in the list have
    95  // a severity of Error.
    96  func (diags Diagnostics) HasErrors() bool {
    97  	for _, diag := range diags {
    98  		if diag.Severity() == Error {
    99  			return true
   100  		}
   101  	}
   102  	return false
   103  }
   104  
   105  // ForRPC returns a version of the receiver that has been simplified so that
   106  // it is friendly to RPC protocols.
   107  //
   108  // Currently this means that it can be serialized with encoding/gob and
   109  // subsequently re-inflated. It may later grow to include other serialization
   110  // formats.
   111  //
   112  // Note that this loses information about the original objects used to
   113  // construct the diagnostics, so e.g. the errwrap API will not work as
   114  // expected on an error-wrapped Diagnostics that came from ForRPC.
   115  func (diags Diagnostics) ForRPC() Diagnostics {
   116  	ret := make(Diagnostics, len(diags))
   117  	for i := range diags {
   118  		ret[i] = makeRPCFriendlyDiag(diags[i])
   119  	}
   120  	return ret
   121  }
   122  
   123  // Err flattens a diagnostics list into a single Go error, or to nil
   124  // if the diagnostics list does not include any error-level diagnostics.
   125  //
   126  // This can be used to smuggle diagnostics through an API that deals in
   127  // native errors, but unfortunately it will lose naked warnings (warnings
   128  // that aren't accompanied by at least one error) since such APIs have no
   129  // mechanism through which to report these.
   130  //
   131  //     return result, diags.Error()
   132  func (diags Diagnostics) Err() error {
   133  	if !diags.HasErrors() {
   134  		return nil
   135  	}
   136  	return diagnosticsAsError{diags}
   137  }
   138  
   139  type diagnosticsAsError struct {
   140  	Diagnostics
   141  }
   142  
   143  func (dae diagnosticsAsError) Error() string {
   144  	diags := dae.Diagnostics
   145  	switch {
   146  	case len(diags) == 0:
   147  		// should never happen, since we don't create this wrapper if
   148  		// there are no diagnostics in the list.
   149  		return "no errors"
   150  	case len(diags) == 1:
   151  		desc := diags[0].Description()
   152  		if desc.Detail == "" {
   153  			return desc.Summary
   154  		}
   155  		return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
   156  	default:
   157  		var ret bytes.Buffer
   158  		fmt.Fprintf(&ret, "%d problems:\n", len(diags))
   159  		for _, diag := range dae.Diagnostics {
   160  			desc := diag.Description()
   161  			if desc.Detail == "" {
   162  				fmt.Fprintf(&ret, "\n- %s", desc.Summary)
   163  			} else {
   164  				fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
   165  			}
   166  		}
   167  		return ret.String()
   168  	}
   169  }
   170  
   171  // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped
   172  // diagnostics object can be picked apart by errwrap-aware code.
   173  func (dae diagnosticsAsError) WrappedErrors() []error {
   174  	var errs []error
   175  	for _, diag := range dae.Diagnostics {
   176  		if wrapper, isErr := diag.(nativeError); isErr {
   177  			errs = append(errs, wrapper.err)
   178  		}
   179  	}
   180  	return errs
   181  }