diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go index 1e363580a9..8b6bd42fec 100644 --- a/diagnostics/diagnostics.go +++ b/diagnostics/diagnostics.go @@ -10,6 +10,7 @@ import ( "go/types" "io" "path/filepath" + "reflect" "sort" "strings" @@ -23,6 +24,11 @@ import ( type Diagnostic struct { Pos token.Position Msg string + + // Start and end position, if available. For many errors these positions are + // not available, but for some they are. + StartPos token.Position + EndPos token.Position } // One or multiple errors of a particular package. @@ -114,12 +120,22 @@ func createPackageDiagnostic(err error) PackageDiagnostic { func createDiagnostics(err error) []Diagnostic { switch err := err.(type) { case types.Error: - return []Diagnostic{ - { - Pos: err.Fset.Position(err.Pos), - Msg: err.Msg, - }, + diag := Diagnostic{ + Pos: err.Fset.Position(err.Pos), + Msg: err.Msg, + } + // There is a special unexported API since Go 1.16 that provides the + // range (start and end position) where the type error exists. + // There is no promise of backwards compatibility in future Go versions + // so we have to be extra careful here to be resilient. + v := reflect.ValueOf(err) + start := v.FieldByName("go116start") + end := v.FieldByName("go116end") + if start.IsValid() && end.IsValid() && start.Int() != end.Int() { + diag.StartPos = err.Fset.Position(token.Pos(start.Int())) + diag.EndPos = err.Fset.Position(token.Pos(end.Int())) } + return []Diagnostic{diag} case scanner.Error: return []Diagnostic{ { @@ -188,25 +204,29 @@ func (diag Diagnostic) WriteTo(w io.Writer, wd string) { fmt.Fprintln(w, diag.Msg) return } - pos := diag.Pos // make a copy - if !strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) && !strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) { - // This file is not from the standard library (either the GOROOT or the - // TINYGOROOT). Make the path relative, for easier reading. Ignore any - // errors in the process (falling back to the absolute path). - pos.Filename = tryToMakePathRelative(pos.Filename, wd) - } + pos := RelativePosition(diag.Pos, wd) fmt.Fprintf(w, "%s: %s\n", pos, diag.Msg) } -// try to make the path relative to the current working directory. If any error -// occurs, this error is ignored and the absolute path is returned instead. -func tryToMakePathRelative(dir, wd string) string { +// Convert the position in pos (assumed to have an absolute path) into a +// relative path if possible. Paths inside GOROOT/TINYGOROOT will remain +// absolute. +func RelativePosition(pos token.Position, wd string) token.Position { + // Check whether we even have a working directory. if wd == "" { - return dir // working directory not found + return pos + } + + // Paths inside GOROOT should be printed in full. + if strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) || strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) { + return pos } - relpath, err := filepath.Rel(wd, dir) - if err != nil { - return dir + + // Make the path relative, for easier reading. Ignore any errors in the + // process (falling back to the absolute path). + relpath, err := filepath.Rel(wd, pos.Filename) + if err == nil { + pos.Filename = relpath } - return relpath + return pos } diff --git a/main.go b/main.go index caea9c56a5..ea922008d5 100644 --- a/main.go +++ b/main.go @@ -1398,6 +1398,8 @@ func printBuildOutput(err error, jsonDiagnostics bool) { ImportPath string Action string Output string `json:",omitempty"` + StartPos string `json:",omitempty"` // non-standard + EndPos string `json:",omitempty"` // non-standard } for _, diags := range diagnostics.CreateDiagnostics(err) { @@ -1407,19 +1409,25 @@ func printBuildOutput(err error, jsonDiagnostics bool) { Action: "build-output", Output: "# " + diags.ImportPath + "\n", }) - os.Stdout.Write(output) - os.Stdout.Write([]byte{'\n'}) + os.Stdout.Write(append(output, '\n')) } for _, diag := range diags.Diagnostics { w := &bytes.Buffer{} diag.WriteTo(w, workingDir) - output, _ := json.Marshal(jsonDiagnosticOutput{ + data := jsonDiagnosticOutput{ ImportPath: diags.ImportPath, Action: "build-output", Output: w.String(), - }) - os.Stdout.Write(output) - os.Stdout.Write([]byte{'\n'}) + } + if diag.StartPos.IsValid() && diag.EndPos.IsValid() { + // Include the non-standard StartPos/EndPos values. These + // are useful for the TinyGo Playground to show better error + // messages. + data.StartPos = diagnostics.RelativePosition(diag.StartPos, workingDir).String() + data.EndPos = diagnostics.RelativePosition(diag.EndPos, workingDir).String() + } + output, _ := json.Marshal(data) + os.Stdout.Write(append(output, '\n')) } // Emit the "Action":"build-fail" JSON. @@ -1427,8 +1435,7 @@ func printBuildOutput(err error, jsonDiagnostics bool) { ImportPath: diags.ImportPath, Action: "build-fail", }) - os.Stdout.Write(output) - os.Stdout.Write([]byte{'\n'}) + os.Stdout.Write(append(output, '\n')) } os.Exit(1) }