| package tfdiags |
| |
| import ( |
| "bytes" |
| "fmt" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| "github.com/hashicorp/errwrap" |
| multierror "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/hcl/v2" |
| ) |
| |
| // Diagnostics is a list of diagnostics. Diagnostics is intended to be used |
| // where a Go "error" might normally be used, allowing richer information |
| // to be conveyed (more context, support for warnings). |
| // |
| // A nil Diagnostics is a valid, empty diagnostics list, thus allowing |
| // heap allocation to be avoided in the common case where there are no |
| // diagnostics to report at all. |
| type Diagnostics []Diagnostic |
| |
| // Append is the main interface for constructing Diagnostics lists, taking |
| // an existing list (which may be nil) and appending the new objects to it |
| // after normalizing them to be implementations of Diagnostic. |
| // |
| // The usual pattern for a function that natively "speaks" diagnostics is: |
| // |
| // // Create a nil Diagnostics at the start of the function |
| // var diags diag.Diagnostics |
| // |
| // // At later points, build on it if errors / warnings occur: |
| // foo, err := DoSomethingRisky() |
| // if err != nil { |
| // diags = diags.Append(err) |
| // } |
| // |
| // // Eventually return the result and diagnostics in place of error |
| // return result, diags |
| // |
| // Append accepts a variety of different diagnostic-like types, including |
| // native Go errors and HCL diagnostics. It also knows how to unwrap |
| // a multierror.Error into separate error diagnostics. It can be passed |
| // another Diagnostics to concatenate the two lists. If given something |
| // it cannot handle, this function will panic. |
| func (diags Diagnostics) Append(new ...interface{}) Diagnostics { |
| for _, item := range new { |
| if item == nil { |
| continue |
| } |
| |
| switch ti := item.(type) { |
| case Diagnostic: |
| diags = append(diags, ti) |
| case Diagnostics: |
| diags = append(diags, ti...) // flatten |
| case diagnosticsAsError: |
| diags = diags.Append(ti.Diagnostics) // unwrap |
| case NonFatalError: |
| diags = diags.Append(ti.Diagnostics) // unwrap |
| case hcl.Diagnostics: |
| for _, hclDiag := range ti { |
| diags = append(diags, hclDiagnostic{hclDiag}) |
| } |
| case *hcl.Diagnostic: |
| diags = append(diags, hclDiagnostic{ti}) |
| case *multierror.Error: |
| for _, err := range ti.Errors { |
| diags = append(diags, nativeError{err}) |
| } |
| case error: |
| switch { |
| case errwrap.ContainsType(ti, Diagnostics(nil)): |
| // If we have an errwrap wrapper with a Diagnostics hiding |
| // inside then we'll unpick it here to get access to the |
| // individual diagnostics. |
| diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil))) |
| case errwrap.ContainsType(ti, hcl.Diagnostics(nil)): |
| // Likewise, if we have HCL diagnostics we'll unpick that too. |
| diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil))) |
| default: |
| diags = append(diags, nativeError{ti}) |
| } |
| default: |
| panic(fmt.Errorf("can't construct diagnostic(s) from %T", item)) |
| } |
| } |
| |
| // Given the above, we should never end up with a non-nil empty slice |
| // here, but we'll make sure of that so callers can rely on empty == nil |
| if len(diags) == 0 { |
| return nil |
| } |
| |
| return diags |
| } |
| |
| // HasErrors returns true if any of the diagnostics in the list have |
| // a severity of Error. |
| func (diags Diagnostics) HasErrors() bool { |
| for _, diag := range diags { |
| if diag.Severity() == Error { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // ForRPC returns a version of the receiver that has been simplified so that |
| // it is friendly to RPC protocols. |
| // |
| // Currently this means that it can be serialized with encoding/gob and |
| // subsequently re-inflated. It may later grow to include other serialization |
| // formats. |
| // |
| // Note that this loses information about the original objects used to |
| // construct the diagnostics, so e.g. the errwrap API will not work as |
| // expected on an error-wrapped Diagnostics that came from ForRPC. |
| func (diags Diagnostics) ForRPC() Diagnostics { |
| ret := make(Diagnostics, len(diags)) |
| for i := range diags { |
| ret[i] = makeRPCFriendlyDiag(diags[i]) |
| } |
| return ret |
| } |
| |
| // Err flattens a diagnostics list into a single Go error, or to nil |
| // if the diagnostics list does not include any error-level diagnostics. |
| // |
| // This can be used to smuggle diagnostics through an API that deals in |
| // native errors, but unfortunately it will lose any warnings that aren't |
| // accompanied by at least one error since such APIs have no mechanism through |
| // which to report those. |
| // |
| // return result, diags.Error() |
| func (diags Diagnostics) Err() error { |
| if !diags.HasErrors() { |
| return nil |
| } |
| return diagnosticsAsError{diags} |
| } |
| |
| // ErrWithWarnings is similar to Err except that it will also return a non-nil |
| // error if the receiver contains only warnings. |
| // |
| // In the warnings-only situation, the result is guaranteed to be of dynamic |
| // type NonFatalError, allowing diagnostics-aware callers to type-assert |
| // and unwrap it, treating it as non-fatal. |
| // |
| // This should be used only in contexts where the caller is able to recognize |
| // and handle NonFatalError. For normal callers that expect a lack of errors |
| // to be signaled by nil, use just Diagnostics.Err. |
| func (diags Diagnostics) ErrWithWarnings() error { |
| if len(diags) == 0 { |
| return nil |
| } |
| if diags.HasErrors() { |
| return diags.Err() |
| } |
| return NonFatalError{diags} |
| } |
| |
| // NonFatalErr is similar to Err except that it always returns either nil |
| // (if there are no diagnostics at all) or NonFatalError. |
| // |
| // This allows diagnostics to be returned over an error return channel while |
| // being explicit that the diagnostics should not halt processing. |
| // |
| // This should be used only in contexts where the caller is able to recognize |
| // and handle NonFatalError. For normal callers that expect a lack of errors |
| // to be signaled by nil, use just Diagnostics.Err. |
| func (diags Diagnostics) NonFatalErr() error { |
| if len(diags) == 0 { |
| return nil |
| } |
| return NonFatalError{diags} |
| } |
| |
| // Sort applies an ordering to the diagnostics in the receiver in-place. |
| // |
| // The ordering is: warnings before errors, sourceless before sourced, |
| // short source paths before long source paths, and then ordering by |
| // position within each file. |
| // |
| // Diagnostics that do not differ by any of these sortable characteristics |
| // will remain in the same relative order after this method returns. |
| func (diags Diagnostics) Sort() { |
| sort.Stable(sortDiagnostics(diags)) |
| } |
| |
| type diagnosticsAsError struct { |
| Diagnostics |
| } |
| |
| func (dae diagnosticsAsError) Error() string { |
| diags := dae.Diagnostics |
| switch { |
| case len(diags) == 0: |
| // should never happen, since we don't create this wrapper if |
| // there are no diagnostics in the list. |
| return "no errors" |
| case len(diags) == 1: |
| desc := diags[0].Description() |
| if desc.Detail == "" { |
| return desc.Summary |
| } |
| return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail) |
| default: |
| var ret bytes.Buffer |
| fmt.Fprintf(&ret, "%d problems:\n", len(diags)) |
| for _, diag := range dae.Diagnostics { |
| desc := diag.Description() |
| if desc.Detail == "" { |
| fmt.Fprintf(&ret, "\n- %s", desc.Summary) |
| } else { |
| fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail) |
| } |
| } |
| return ret.String() |
| } |
| } |
| |
| // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped |
| // diagnostics object can be picked apart by errwrap-aware code. |
| func (dae diagnosticsAsError) WrappedErrors() []error { |
| var errs []error |
| for _, diag := range dae.Diagnostics { |
| if wrapper, isErr := diag.(nativeError); isErr { |
| errs = append(errs, wrapper.err) |
| } |
| } |
| return errs |
| } |
| |
| // NonFatalError is a special error type, returned by |
| // Diagnostics.ErrWithWarnings and Diagnostics.NonFatalErr, |
| // that indicates that the wrapped diagnostics should be treated as non-fatal. |
| // Callers can conditionally type-assert an error to this type in order to |
| // detect the non-fatal scenario and handle it in a different way. |
| type NonFatalError struct { |
| Diagnostics |
| } |
| |
| func (woe NonFatalError) Error() string { |
| diags := woe.Diagnostics |
| switch { |
| case len(diags) == 0: |
| // should never happen, since we don't create this wrapper if |
| // there are no diagnostics in the list. |
| return "no errors or warnings" |
| case len(diags) == 1: |
| desc := diags[0].Description() |
| if desc.Detail == "" { |
| return desc.Summary |
| } |
| return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail) |
| default: |
| var ret bytes.Buffer |
| if diags.HasErrors() { |
| fmt.Fprintf(&ret, "%d problems:\n", len(diags)) |
| } else { |
| fmt.Fprintf(&ret, "%d warnings:\n", len(diags)) |
| } |
| for _, diag := range woe.Diagnostics { |
| desc := diag.Description() |
| if desc.Detail == "" { |
| fmt.Fprintf(&ret, "\n- %s", desc.Summary) |
| } else { |
| fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail) |
| } |
| } |
| return ret.String() |
| } |
| } |
| |
| // sortDiagnostics is an implementation of sort.Interface |
| type sortDiagnostics []Diagnostic |
| |
| var _ sort.Interface = sortDiagnostics(nil) |
| |
| func (sd sortDiagnostics) Len() int { |
| return len(sd) |
| } |
| |
| func (sd sortDiagnostics) Less(i, j int) bool { |
| iD, jD := sd[i], sd[j] |
| iSev, jSev := iD.Severity(), jD.Severity() |
| iSrc, jSrc := iD.Source(), jD.Source() |
| |
| switch { |
| |
| case iSev != jSev: |
| return iSev == Warning |
| |
| case (iSrc.Subject == nil) != (jSrc.Subject == nil): |
| return iSrc.Subject == nil |
| |
| case iSrc.Subject != nil && *iSrc.Subject != *jSrc.Subject: |
| iSubj := iSrc.Subject |
| jSubj := jSrc.Subject |
| switch { |
| case iSubj.Filename != jSubj.Filename: |
| // Path with fewer segments goes first if they are different lengths |
| sep := string(filepath.Separator) |
| iCount := strings.Count(iSubj.Filename, sep) |
| jCount := strings.Count(jSubj.Filename, sep) |
| if iCount != jCount { |
| return iCount < jCount |
| } |
| return iSubj.Filename < jSubj.Filename |
| case iSubj.Start.Byte != jSubj.Start.Byte: |
| return iSubj.Start.Byte < jSubj.Start.Byte |
| case iSubj.End.Byte != jSubj.End.Byte: |
| return iSubj.End.Byte < jSubj.End.Byte |
| } |
| fallthrough |
| |
| default: |
| // The remaining properties do not have a defined ordering, so |
| // we'll leave it unspecified. Since we use sort.Stable in |
| // the caller of this, the ordering of remaining items will |
| // be preserved. |
| return false |
| } |
| } |
| |
| func (sd sortDiagnostics) Swap(i, j int) { |
| sd[i], sd[j] = sd[j], sd[i] |
| } |