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