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