From 0742e756e5dec0252dbc749953aaa869df231025 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 20 Jun 2018 18:57:23 -0700 Subject: [PATCH] tfdiags: Sort order for diagnostics Because we gather together diagnostics from many different parts of the codebase, the list often ends up being in a non-ideal order. Here we define a partial ordering for diagnostics that should hopefully make them easier to scan when many are present, by grouping together diagnostics that are of the same severity and belong to the same file. We use sort.Stable here because we have a partial order and so we need to make sure that diagnostics that do not have a relative ordering will remain in their original order. This sorting is applied just in time before rendering the diagnostics in command.Meta.showDiagnostics. --- command/meta.go | 1 + tfdiags/diagnostics.go | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/command/meta.go b/command/meta.go index 3c48942d8..aa7e8972f 100644 --- a/command/meta.go +++ b/command/meta.go @@ -488,6 +488,7 @@ func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) { func (m *Meta) showDiagnostics(vals ...interface{}) { var diags tfdiags.Diagnostics diags = diags.Append(vals...) + diags.Sort() for _, diag := range diags { // TODO: Actually measure the terminal width and pass it here. diff --git a/tfdiags/diagnostics.go b/tfdiags/diagnostics.go index 580f368ad..465b230f6 100644 --- a/tfdiags/diagnostics.go +++ b/tfdiags/diagnostics.go @@ -3,6 +3,9 @@ package tfdiags import ( "bytes" "fmt" + "path/filepath" + "sort" + "strings" "github.com/hashicorp/errwrap" multierror "github.com/hashicorp/go-multierror" @@ -174,6 +177,18 @@ func (diags Diagnostics) NonFatalErr() error { 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 } @@ -258,3 +273,58 @@ func (woe NonFatalError) Error() string { 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] +}