Skip to content

main: add StartPos and EndPos to -json output #4868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"go/types"
"io"
"path/filepath"
"reflect"
"sort"
"strings"

Expand All @@ -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.
Expand Down Expand Up @@ -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{
{
Expand Down Expand Up @@ -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
}
23 changes: 15 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -1407,28 +1409,33 @@ 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.
output, _ := json.Marshal(jsonDiagnosticOutput{
ImportPath: diags.ImportPath,
Action: "build-fail",
})
os.Stdout.Write(output)
os.Stdout.Write([]byte{'\n'})
os.Stdout.Write(append(output, '\n'))
}
os.Exit(1)
}
Expand Down