diagnostics.go raw

   1  // Package diagnostics formats compiler errors and prints them in a consistent
   2  // way.
   3  package diagnostics
   4  
   5  import (
   6  	"bytes"
   7  	"fmt"
   8  	"go/scanner"
   9  	"go/token"
  10  	"go/types"
  11  	"io"
  12  	"path/filepath"
  13  	"reflect"
  14  	"sort"
  15  	"strings"
  16  
  17  	"moxie/builder"
  18  	"moxie/goenv"
  19  	"moxie/interp"
  20  	"moxie/loader"
  21  )
  22  
  23  // A single diagnostic.
  24  type Diagnostic struct {
  25  	Pos token.Position
  26  	Msg string
  27  
  28  	// Start and end position, if available. For many errors these positions are
  29  	// not available, but for some they are.
  30  	StartPos token.Position
  31  	EndPos   token.Position
  32  }
  33  
  34  // One or multiple errors of a particular package.
  35  // It can also represent whole-program errors (like linker errors) that can't
  36  // easily be connected to a single package.
  37  type PackageDiagnostic struct {
  38  	ImportPath  string // the same ImportPath as in `go list -json`
  39  	Diagnostics []Diagnostic
  40  }
  41  
  42  // Diagnostics of a whole program. This can include errors belonging to multiple
  43  // packages, or just a single package.
  44  type ProgramDiagnostic []PackageDiagnostic
  45  
  46  // CreateDiagnostics reads the underlying errors in the error object and creates
  47  // a set of diagnostics that's sorted and can be readily printed.
  48  func CreateDiagnostics(err error) ProgramDiagnostic {
  49  	if err == nil {
  50  		return nil
  51  	}
  52  	// Right now, the compiler will only show errors for the first package that
  53  	// fails to build. This is likely to change in the future.
  54  	return ProgramDiagnostic{
  55  		createPackageDiagnostic(err),
  56  	}
  57  }
  58  
  59  // Create diagnostics for a single package (though, in practice, it may also be
  60  // used for whole-program diagnostics in some cases).
  61  func createPackageDiagnostic(err error) PackageDiagnostic {
  62  	// Extract diagnostics for this package.
  63  	var pkgDiag PackageDiagnostic
  64  	switch err := err.(type) {
  65  	case *builder.MultiError:
  66  		if err.ImportPath != "" {
  67  			pkgDiag.ImportPath = err.ImportPath
  68  		}
  69  		for _, err := range err.Errs {
  70  			diags := createDiagnostics(err)
  71  			pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, diags...)
  72  		}
  73  	case loader.Errors:
  74  		if err.Pkg != nil {
  75  			pkgDiag.ImportPath = err.Pkg.ImportPath
  76  		}
  77  		for _, err := range err.Errs {
  78  			diags := createDiagnostics(err)
  79  			pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, diags...)
  80  		}
  81  	case *interp.Error:
  82  		pkgDiag.ImportPath = err.ImportPath
  83  		w := &bytes.Buffer{}
  84  		fmt.Fprintln(w, err.Error())
  85  		if len(err.Inst) != 0 {
  86  			fmt.Fprintln(w, err.Inst)
  87  		}
  88  		if len(err.Traceback) > 0 {
  89  			fmt.Fprintln(w, "\ntraceback:")
  90  			for _, line := range err.Traceback {
  91  				fmt.Fprintln(w, line.Pos.String()+":")
  92  				fmt.Fprintln(w, line.Inst)
  93  			}
  94  		}
  95  		pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, Diagnostic{
  96  			Msg: w.String(),
  97  		})
  98  	default:
  99  		pkgDiag.Diagnostics = createDiagnostics(err)
 100  	}
 101  
 102  	// Sort these diagnostics by file/line/column.
 103  	sort.SliceStable(pkgDiag.Diagnostics, func(i, j int) bool {
 104  		posI := pkgDiag.Diagnostics[i].Pos
 105  		posJ := pkgDiag.Diagnostics[j].Pos
 106  		if posI.Filename != posJ.Filename {
 107  			return posI.Filename < posJ.Filename
 108  		}
 109  		if posI.Line != posJ.Line {
 110  			return posI.Line < posJ.Line
 111  		}
 112  		return posI.Column < posJ.Column
 113  	})
 114  
 115  	return pkgDiag
 116  }
 117  
 118  // Extract diagnostics from the given error message and return them as a slice
 119  // of errors (which in many cases will just be a single diagnostic).
 120  func createDiagnostics(err error) []Diagnostic {
 121  	switch err := err.(type) {
 122  	case types.Error:
 123  		diag := Diagnostic{
 124  			Pos: err.Fset.Position(err.Pos),
 125  			Msg: err.Msg,
 126  		}
 127  		// There is a special unexported API since Go 1.16 that provides the
 128  		// range (start and end position) where the type error exists.
 129  		// There is no promise of backwards compatibility in future Go versions
 130  		// so we have to be extra careful here to be resilient.
 131  		v := reflect.ValueOf(err)
 132  		start := v.FieldByName("go116start")
 133  		end := v.FieldByName("go116end")
 134  		if start.IsValid() && end.IsValid() && start.Int() != end.Int() {
 135  			diag.StartPos = err.Fset.Position(token.Pos(start.Int()))
 136  			diag.EndPos = err.Fset.Position(token.Pos(end.Int()))
 137  		}
 138  		return []Diagnostic{diag}
 139  	case scanner.Error:
 140  		return []Diagnostic{
 141  			{
 142  				Pos: err.Pos,
 143  				Msg: err.Msg,
 144  			},
 145  		}
 146  	case scanner.ErrorList:
 147  		var diags []Diagnostic
 148  		for _, err := range err {
 149  			diags = append(diags, createDiagnostics(*err)...)
 150  		}
 151  		return diags
 152  	case loader.Error:
 153  		if err.Err.Pos.Filename != "" {
 154  			// Probably a syntax error in a dependency.
 155  			return createDiagnostics(err.Err)
 156  		} else {
 157  			// Probably an "import cycle not allowed" error.
 158  			buf := &bytes.Buffer{}
 159  			fmt.Fprintln(buf, "package", err.ImportStack[0])
 160  			for i := 1; i < len(err.ImportStack); i++ {
 161  				pkgPath := err.ImportStack[i]
 162  				if i == len(err.ImportStack)-1 {
 163  					// last package
 164  					fmt.Fprintln(buf, "\timports", pkgPath+": "+err.Err.Error())
 165  				} else {
 166  					// not the last package
 167  					fmt.Fprintln(buf, "\timports", pkgPath)
 168  				}
 169  			}
 170  			return []Diagnostic{
 171  				{Msg: buf.String()},
 172  			}
 173  		}
 174  	default:
 175  		return []Diagnostic{
 176  			{Msg: err.Error()},
 177  		}
 178  	}
 179  }
 180  
 181  // Write program diagnostics to the given writer with 'wd' as the relative
 182  // working directory.
 183  func (progDiag ProgramDiagnostic) WriteTo(w io.Writer, wd string) {
 184  	for _, pkgDiag := range progDiag {
 185  		pkgDiag.WriteTo(w, wd)
 186  	}
 187  }
 188  
 189  // Write package diagnostics to the given writer with 'wd' as the relative
 190  // working directory.
 191  func (pkgDiag PackageDiagnostic) WriteTo(w io.Writer, wd string) {
 192  	if pkgDiag.ImportPath != "" {
 193  		fmt.Fprintln(w, "#", pkgDiag.ImportPath)
 194  	}
 195  	for _, diag := range pkgDiag.Diagnostics {
 196  		diag.WriteTo(w, wd)
 197  	}
 198  }
 199  
 200  // Write this diagnostic to the given writer with 'wd' as the relative working
 201  // directory.
 202  func (diag Diagnostic) WriteTo(w io.Writer, wd string) {
 203  	if diag.Pos == (token.Position{}) {
 204  		fmt.Fprintln(w, diag.Msg)
 205  		return
 206  	}
 207  	pos := RelativePosition(diag.Pos, wd)
 208  	fmt.Fprintf(w, "%s: %s\n", pos, diag.Msg)
 209  }
 210  
 211  // Convert the position in pos (assumed to have an absolute path) into a
 212  // relative path if possible. Paths inside GOROOT/MOXIEROOT will remain
 213  // absolute.
 214  func RelativePosition(pos token.Position, wd string) token.Position {
 215  	// Check whether we even have a working directory.
 216  	if wd == "" {
 217  		return pos
 218  	}
 219  
 220  	// Paths inside GOROOT should be printed in full.
 221  	if strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) || strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("MOXIEROOT"), "src")) {
 222  		return pos
 223  	}
 224  
 225  	// Make the path relative, for easier reading. Ignore any errors in the
 226  	// process (falling back to the absolute path).
 227  	relpath, err := filepath.Rel(wd, pos.Filename)
 228  	if err == nil {
 229  		pos.Filename = relpath
 230  	}
 231  	return pos
 232  }
 233