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