aboutsummaryrefslogtreecommitdiffhomepage
path: root/diagnostics/diagnostics.go
blob: c793ba4aa624963ef280a74616a11a8811b5c4f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Package diagnostics formats compiler errors and prints them in a consistent
// way.
package diagnostics

import (
	"bytes"
	"fmt"
	"go/scanner"
	"go/token"
	"go/types"
	"io"
	"path/filepath"
	"strings"

	"github.com/tinygo-org/tinygo/builder"
	"github.com/tinygo-org/tinygo/goenv"
	"github.com/tinygo-org/tinygo/interp"
	"github.com/tinygo-org/tinygo/loader"
)

// A single diagnostic.
type Diagnostic struct {
	Pos token.Position
	Msg string
}

// One or multiple errors of a particular package.
// It can also represent whole-program errors (like linker errors) that can't
// easily be connected to a single package.
type PackageDiagnostic struct {
	ImportPath  string // the same ImportPath as in `go list -json`
	Diagnostics []Diagnostic
}

// Diagnostics of a whole program. This can include errors belonging to multiple
// packages, or just a single package.
type ProgramDiagnostic []PackageDiagnostic

// CreateDiagnostics reads the underlying errors in the error object and creates
// a set of diagnostics that's sorted and can be readily printed.
func CreateDiagnostics(err error) ProgramDiagnostic {
	if err == nil {
		return nil
	}
	switch err := err.(type) {
	case *builder.MultiError:
		var diags ProgramDiagnostic
		for _, err := range err.Errs {
			diags = append(diags, createPackageDiagnostic(err))
		}
		return diags
	default:
		return ProgramDiagnostic{
			createPackageDiagnostic(err),
		}
	}
}

// Create diagnostics for a single package (though, in practice, it may also be
// used for whole-program diagnostics in some cases).
func createPackageDiagnostic(err error) PackageDiagnostic {
	var pkgDiag PackageDiagnostic
	switch err := err.(type) {
	case loader.Errors:
		if err.Pkg != nil {
			pkgDiag.ImportPath = err.Pkg.ImportPath
		}
		for _, err := range err.Errs {
			diags := createDiagnostics(err)
			pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, diags...)
		}
	case *interp.Error:
		pkgDiag.ImportPath = err.ImportPath
		w := &bytes.Buffer{}
		fmt.Fprintln(w, err.Error())
		if len(err.Inst) != 0 {
			fmt.Fprintln(w, err.Inst)
		}
		if len(err.Traceback) > 0 {
			fmt.Fprintln(w, "\ntraceback:")
			for _, line := range err.Traceback {
				fmt.Fprintln(w, line.Pos.String()+":")
				fmt.Fprintln(w, line.Inst)
			}
		}
		pkgDiag.Diagnostics = append(pkgDiag.Diagnostics, Diagnostic{
			Msg: w.String(),
		})
	default:
		pkgDiag.Diagnostics = createDiagnostics(err)
	}
	// TODO: sort
	return pkgDiag
}

// Extract diagnostics from the given error message and return them as a slice
// of errors (which in many cases will just be a single diagnostic).
func createDiagnostics(err error) []Diagnostic {
	switch err := err.(type) {
	case types.Error:
		return []Diagnostic{
			{
				Pos: err.Fset.Position(err.Pos),
				Msg: err.Msg,
			},
		}
	case scanner.Error:
		return []Diagnostic{
			{
				Pos: err.Pos,
				Msg: err.Msg,
			},
		}
	case scanner.ErrorList:
		var diags []Diagnostic
		for _, err := range err {
			diags = append(diags, createDiagnostics(*err)...)
		}
		return diags
	case loader.Error:
		if err.Err.Pos.Filename != "" {
			// Probably a syntax error in a dependency.
			return createDiagnostics(err.Err)
		} else {
			// Probably an "import cycle not allowed" error.
			buf := &bytes.Buffer{}
			fmt.Fprintln(buf, "package", err.ImportStack[0])
			for i := 1; i < len(err.ImportStack); i++ {
				pkgPath := err.ImportStack[i]
				if i == len(err.ImportStack)-1 {
					// last package
					fmt.Fprintln(buf, "\timports", pkgPath+": "+err.Err.Error())
				} else {
					// not the last pacakge
					fmt.Fprintln(buf, "\timports", pkgPath)
				}
			}
			return []Diagnostic{
				{Msg: buf.String()},
			}
		}
	default:
		return []Diagnostic{
			{Msg: err.Error()},
		}
	}
}

// Write program diagnostics to the given writer with 'wd' as the relative
// working directory.
func (progDiag ProgramDiagnostic) WriteTo(w io.Writer, wd string) {
	for _, pkgDiag := range progDiag {
		pkgDiag.WriteTo(w, wd)
	}
}

// Write package diagnostics to the given writer with 'wd' as the relative
// working directory.
func (pkgDiag PackageDiagnostic) WriteTo(w io.Writer, wd string) {
	if pkgDiag.ImportPath != "" {
		fmt.Fprintln(w, "#", pkgDiag.ImportPath)
	}
	for _, diag := range pkgDiag.Diagnostics {
		diag.WriteTo(w, wd)
	}
}

// Write this diagnostic to the given writer with 'wd' as the relative working
// directory.
func (diag Diagnostic) WriteTo(w io.Writer, wd string) {
	if diag.Pos == (token.Position{}) {
		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)
	}
	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 {
	if wd == "" {
		return dir // working directory not found
	}
	relpath, err := filepath.Rel(wd, dir)
	if err != nil {
		return dir
	}
	return relpath
}