Skip to content

Commit 80269b9

Browse files
aykevldeadprogram
authored andcommitted
diagnostics: move diagnostic printing to a new package
This is a refactor, which should (in theory) not change the behavior of the compiler. But since this is a pretty large change, there is a chance there will be some regressions. For that reason, the previous commits added a bunch of tests to make sure most error messages will not be changed due to this refactor.
1 parent 3788b31 commit 80269b9

File tree

4 files changed

+207
-98
lines changed

4 files changed

+207
-98
lines changed

diagnostics/diagnostics.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
"strings"
14+
15+
"github.com/tinygo-org/tinygo/builder"
16+
"github.com/tinygo-org/tinygo/goenv"
17+
"github.com/tinygo-org/tinygo/interp"
18+
"github.com/tinygo-org/tinygo/loader"
19+
)
20+
21+
// A single diagnostic.
22+
type Diagnostic struct {
23+
Pos token.Position
24+
Msg string
25+
}
26+
27+
// One or multiple errors of a particular package.
28+
// It can also represent whole-program errors (like linker errors) that can't
29+
// easily be connected to a single package.
30+
type PackageDiagnostic struct {
31+
ImportPath string // the same ImportPath as in `go list -json`
32+
Diagnostics []Diagnostic
33+
}
34+
35+
// Diagnostics of a whole program. This can include errors belonging to multiple
36+
// packages, or just a single package.
37+
type ProgramDiagnostic []PackageDiagnostic
38+
39+
// CreateDiagnostics reads the underlying errors in the error object and creates
40+
// a set of diagnostics that's sorted and can be readily printed.
41+
func CreateDiagnostics(err error) ProgramDiagnostic {
42+
if err == nil {
43+
return nil
44+
}
45+
switch err := err.(type) {
46+
case *builder.MultiError:
47+
var diags ProgramDiagnostic
48+
for _, err := range err.Errs {
49+
diags = append(diags, createPackageDiagnostic(err))
50+
}
51+
return diags
52+
default:
53+
return ProgramDiagnostic{
54+
createPackageDiagnostic(err),
55+
}
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+
var pkgDiag PackageDiagnostic
63+
switch err := err.(type) {
64+
case loader.Errors:
65+
if err.Pkg != nil {
66+
pkgDiag.ImportPath = err.Pkg.ImportPath
67+
}
68+
for _, err := range err.Errs {
69+
diags := createDiagnostics(err)
70+
pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, diags...)
71+
}
72+
case *interp.Error:
73+
pkgDiag.ImportPath = err.ImportPath
74+
w := &bytes.Buffer{}
75+
fmt.Fprintln(w, err.Error())
76+
if len(err.Inst) != 0 {
77+
fmt.Fprintln(w, err.Inst)
78+
}
79+
if len(err.Traceback) > 0 {
80+
fmt.Fprintln(w, "\ntraceback:")
81+
for _, line := range err.Traceback {
82+
fmt.Fprintln(w, line.Pos.String()+":")
83+
fmt.Fprintln(w, line.Inst)
84+
}
85+
}
86+
pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, Diagnostic{
87+
Msg: w.String(),
88+
})
89+
default:
90+
pkgDiag.Diagnostics = createDiagnostics(err)
91+
}
92+
// TODO: sort
93+
return pkgDiag
94+
}
95+
96+
// Extract diagnostics from the given error message and return them as a slice
97+
// of errors (which in many cases will just be a single diagnostic).
98+
func createDiagnostics(err error) []Diagnostic {
99+
switch err := err.(type) {
100+
case types.Error:
101+
return []Diagnostic{
102+
{
103+
Pos: err.Fset.Position(err.Pos),
104+
Msg: err.Msg,
105+
},
106+
}
107+
case scanner.Error:
108+
return []Diagnostic{
109+
{
110+
Pos: err.Pos,
111+
Msg: err.Msg,
112+
},
113+
}
114+
case scanner.ErrorList:
115+
var diags []Diagnostic
116+
for _, err := range err {
117+
diags = append(diags, createDiagnostics(*err)...)
118+
}
119+
return diags
120+
case loader.Error:
121+
if err.Err.Pos.Filename != "" {
122+
// Probably a syntax error in a dependency.
123+
return createDiagnostics(err.Err)
124+
} else {
125+
// Probably an "import cycle not allowed" error.
126+
buf := &bytes.Buffer{}
127+
fmt.Fprintln(buf, "package", err.ImportStack[0])
128+
for i := 1; i < len(err.ImportStack); i++ {
129+
pkgPath := err.ImportStack[i]
130+
if i == len(err.ImportStack)-1 {
131+
// last package
132+
fmt.Fprintln(buf, "\timports", pkgPath+": "+err.Err.Error())
133+
} else {
134+
// not the last pacakge
135+
fmt.Fprintln(buf, "\timports", pkgPath)
136+
}
137+
}
138+
return []Diagnostic{
139+
{Msg: buf.String()},
140+
}
141+
}
142+
default:
143+
return []Diagnostic{
144+
{Msg: err.Error()},
145+
}
146+
}
147+
}
148+
149+
// Write program diagnostics to the given writer with 'wd' as the relative
150+
// working directory.
151+
func (progDiag ProgramDiagnostic) WriteTo(w io.Writer, wd string) {
152+
for _, pkgDiag := range progDiag {
153+
pkgDiag.WriteTo(w, wd)
154+
}
155+
}
156+
157+
// Write package diagnostics to the given writer with 'wd' as the relative
158+
// working directory.
159+
func (pkgDiag PackageDiagnostic) WriteTo(w io.Writer, wd string) {
160+
if pkgDiag.ImportPath != "" {
161+
fmt.Fprintln(w, "#", pkgDiag.ImportPath)
162+
}
163+
for _, diag := range pkgDiag.Diagnostics {
164+
diag.WriteTo(w, wd)
165+
}
166+
}
167+
168+
// Write this diagnostic to the given writer with 'wd' as the relative working
169+
// directory.
170+
func (diag Diagnostic) WriteTo(w io.Writer, wd string) {
171+
if diag.Pos == (token.Position{}) {
172+
fmt.Fprintln(w, diag.Msg)
173+
return
174+
}
175+
pos := diag.Pos // make a copy
176+
if !strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) && !strings.HasPrefix(pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) {
177+
// This file is not from the standard library (either the GOROOT or the
178+
// TINYGOROOT). Make the path relative, for easier reading. Ignore any
179+
// errors in the process (falling back to the absolute path).
180+
pos.Filename = tryToMakePathRelative(pos.Filename, wd)
181+
}
182+
fmt.Fprintf(w, "%s: %s\n", pos, diag.Msg)
183+
}
184+
185+
// try to make the path relative to the current working directory. If any error
186+
// occurs, this error is ignored and the absolute path is returned instead.
187+
func tryToMakePathRelative(dir, wd string) string {
188+
if wd == "" {
189+
return dir // working directory not found
190+
}
191+
relpath, err := filepath.Rel(wd, dir)
192+
if err != nil {
193+
return dir
194+
}
195+
return relpath
196+
}

errors_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"bytes"
5-
"fmt"
65
"os"
76
"path/filepath"
87
"regexp"
@@ -11,6 +10,7 @@ import (
1110
"time"
1211

1312
"github.com/tinygo-org/tinygo/compileopts"
13+
"github.com/tinygo-org/tinygo/diagnostics"
1414
)
1515

1616
// Test the error messages of the TinyGo compiler.
@@ -59,9 +59,7 @@ func testErrorMessages(t *testing.T, filename string) {
5959

6060
// Write error message out as plain text.
6161
var buf bytes.Buffer
62-
printCompilerError(err, func(v ...interface{}) {
63-
fmt.Fprintln(&buf, v...)
64-
}, wd)
62+
diagnostics.CreateDiagnostics(err).WriteTo(&buf, wd)
6563
actual := strings.TrimRight(buf.String(), "\n")
6664

6765
// Check whether the error is as expected.

main.go

Lines changed: 3 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"errors"
99
"flag"
1010
"fmt"
11-
"go/scanner"
12-
"go/types"
1311
"io"
1412
"os"
1513
"os/exec"
@@ -30,8 +28,8 @@ import (
3028
"github.com/mattn/go-colorable"
3129
"github.com/tinygo-org/tinygo/builder"
3230
"github.com/tinygo-org/tinygo/compileopts"
31+
"github.com/tinygo-org/tinygo/diagnostics"
3332
"github.com/tinygo-org/tinygo/goenv"
34-
"github.com/tinygo-org/tinygo/interp"
3533
"github.com/tinygo-org/tinygo/loader"
3634
"golang.org/x/tools/go/buildutil"
3735
"tinygo.org/x/go-llvm"
@@ -1292,99 +1290,13 @@ func usage(command string) {
12921290
}
12931291
}
12941292

1295-
// try to make the path relative to the current working directory. If any error
1296-
// occurs, this error is ignored and the absolute path is returned instead.
1297-
func tryToMakePathRelative(dir, wd string) string {
1298-
if wd == "" {
1299-
return dir // working directory not found
1300-
}
1301-
relpath, err := filepath.Rel(wd, dir)
1302-
if err != nil {
1303-
return dir
1304-
}
1305-
return relpath
1306-
}
1307-
1308-
// printCompilerError prints compiler errors using the provided logger function
1309-
// (similar to fmt.Println).
1310-
func printCompilerError(err error, logln func(...interface{}), wd string) {
1311-
switch err := err.(type) {
1312-
case types.Error:
1313-
printCompilerError(scanner.Error{
1314-
Pos: err.Fset.Position(err.Pos),
1315-
Msg: err.Msg,
1316-
}, logln, wd)
1317-
case scanner.Error:
1318-
if !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) && !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) {
1319-
// This file is not from the standard library (either the GOROOT or
1320-
// the TINYGOROOT). Make the path relative, for easier reading.
1321-
// Ignore any errors in the process (falling back to the absolute
1322-
// path).
1323-
err.Pos.Filename = tryToMakePathRelative(err.Pos.Filename, wd)
1324-
}
1325-
logln(err)
1326-
case scanner.ErrorList:
1327-
for _, scannerErr := range err {
1328-
printCompilerError(*scannerErr, logln, wd)
1329-
}
1330-
case *interp.Error:
1331-
logln("#", err.ImportPath)
1332-
logln(err.Error())
1333-
if len(err.Inst) != 0 {
1334-
logln(err.Inst)
1335-
}
1336-
if len(err.Traceback) > 0 {
1337-
logln("\ntraceback:")
1338-
for _, line := range err.Traceback {
1339-
logln(line.Pos.String() + ":")
1340-
logln(line.Inst)
1341-
}
1342-
}
1343-
case loader.Errors:
1344-
// Parser errors, typechecking errors, or `go list` errors.
1345-
// err.Pkg is nil for `go list` errors.
1346-
if err.Pkg != nil {
1347-
logln("#", err.Pkg.ImportPath)
1348-
}
1349-
for _, err := range err.Errs {
1350-
printCompilerError(err, logln, wd)
1351-
}
1352-
case loader.Error:
1353-
if err.Err.Pos.Filename != "" {
1354-
// Probably a syntax error in a dependency.
1355-
printCompilerError(err.Err, logln, wd)
1356-
} else {
1357-
// Probably an "import cycle not allowed" error.
1358-
logln("package", err.ImportStack[0])
1359-
for i := 1; i < len(err.ImportStack); i++ {
1360-
pkgPath := err.ImportStack[i]
1361-
if i == len(err.ImportStack)-1 {
1362-
// last package
1363-
logln("\timports", pkgPath+": "+err.Err.Error())
1364-
} else {
1365-
// not the last pacakge
1366-
logln("\timports", pkgPath)
1367-
}
1368-
}
1369-
}
1370-
case *builder.MultiError:
1371-
for _, err := range err.Errs {
1372-
printCompilerError(err, logln, wd)
1373-
}
1374-
default:
1375-
logln("error:", err)
1376-
}
1377-
}
1378-
13791293
func handleCompilerError(err error) {
13801294
if err != nil {
13811295
wd, getwdErr := os.Getwd()
13821296
if getwdErr != nil {
13831297
wd = ""
13841298
}
1385-
printCompilerError(err, func(args ...interface{}) {
1386-
fmt.Fprintln(os.Stderr, args...)
1387-
}, wd)
1299+
diagnostics.CreateDiagnostics(err).WriteTo(os.Stderr, wd)
13881300
os.Exit(1)
13891301
}
13901302
}
@@ -1790,9 +1702,7 @@ func main() {
17901702
if getwdErr != nil {
17911703
wd = ""
17921704
}
1793-
printCompilerError(err, func(args ...interface{}) {
1794-
fmt.Fprintln(stderr, args...)
1795-
}, wd)
1705+
diagnostics.CreateDiagnostics(err).WriteTo(os.Stderr, wd)
17961706
}
17971707
if !passed {
17981708
select {

main_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/aykevl/go-wasm"
2525
"github.com/tinygo-org/tinygo/builder"
2626
"github.com/tinygo-org/tinygo/compileopts"
27+
"github.com/tinygo-org/tinygo/diagnostics"
2728
"github.com/tinygo-org/tinygo/goenv"
2829
)
2930

@@ -380,7 +381,11 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c
380381
return cmd.Run()
381382
})
382383
if err != nil {
383-
printCompilerError(err, t.Log, "")
384+
w := &bytes.Buffer{}
385+
diagnostics.CreateDiagnostics(err).WriteTo(w, "")
386+
for _, line := range strings.Split(strings.TrimRight(w.String(), "\n"), "\n") {
387+
t.Log(line)
388+
}
384389
t.Fail()
385390
return
386391
}

0 commit comments

Comments
 (0)