Skip to content

Commit b7a4eba

Browse files
Robert-Kolmosa-h
andauthored
fix: map go formatting errors to their locations in templ files (#737)
Co-authored-by: Adrian Hesketh <[email protected]>
1 parent 1ecd566 commit b7a4eba

File tree

4 files changed

+142
-1
lines changed

4 files changed

+142
-1
lines changed

cmd/templ/generatecmd/eventhandler.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"crypto/sha256"
88
"fmt"
99
"go/format"
10+
"go/scanner"
11+
"go/token"
1012
"log/slog"
1113
"os"
1214
"path"
@@ -221,7 +223,8 @@ func (h *FSEventHandler) generate(ctx context.Context, fileName string) (goUpdat
221223

222224
formattedGoCode, err := format.Source(b.Bytes())
223225
if err != nil {
224-
return false, false, nil, fmt.Errorf("%s source formatting error: %w", fileName, err)
226+
err = remapErrorList(err, sourceMap, fileName, targetFileName)
227+
return false, false, nil, fmt.Errorf("%s source formatting error %w", fileName, err)
225228
}
226229

227230
// Hash output, and write out the file if the goCodeHash has changed.
@@ -257,6 +260,29 @@ func (h *FSEventHandler) generate(ctx context.Context, fileName string) (goUpdat
257260
return goUpdated, textUpdated, parsedDiagnostics, err
258261
}
259262

263+
// Takes an error from the formatter and attempts to convert the positions reported in the target file to their positions
264+
// in the source file.
265+
func remapErrorList(err error, sourceMap *parser.SourceMap, fileName string, targetFileName string) error {
266+
list, ok := err.(scanner.ErrorList)
267+
if !ok || len(list) == 0 {
268+
return err
269+
}
270+
for i, e := range list {
271+
// The positions in the source map are off by one line because of the package definition.
272+
srcPos, ok := sourceMap.SourcePositionFromTarget(uint32(e.Pos.Line-1), uint32(e.Pos.Column))
273+
if !ok {
274+
continue
275+
}
276+
list[i].Pos = token.Position{
277+
Filename: fileName,
278+
Offset: int(srcPos.Index),
279+
Line: int(srcPos.Line) + 1,
280+
Column: int(srcPos.Col),
281+
}
282+
}
283+
return list
284+
}
285+
260286
func generateSourceMapVisualisation(ctx context.Context, templFileName, goFileName string, sourceMap *parser.SourceMap) error {
261287
if err := ctx.Err(); err != nil {
262288
return err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package testeventhandler
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"go/scanner"
8+
"go/token"
9+
"io"
10+
"log/slog"
11+
"os"
12+
"testing"
13+
14+
"github.com/a-h/templ/cmd/templ/generatecmd"
15+
"github.com/a-h/templ/generator"
16+
"github.com/fsnotify/fsnotify"
17+
"github.com/google/go-cmp/cmp"
18+
)
19+
20+
func TestErrorLocationMapping(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
rawFileName string
24+
errorPositions []token.Position
25+
}{
26+
{
27+
name: "single error outputs location in srcFile",
28+
rawFileName: "single_error.templ.error",
29+
errorPositions: []token.Position{
30+
{Offset: 46, Line: 3, Column: 20},
31+
},
32+
},
33+
{
34+
name: "multiple errors all output locations in srcFile",
35+
rawFileName: "multiple_errors.templ.error",
36+
errorPositions: []token.Position{
37+
{Offset: 41, Line: 3, Column: 15},
38+
{Offset: 101, Line: 7, Column: 22},
39+
{Offset: 126, Line: 10, Column: 1},
40+
},
41+
},
42+
}
43+
44+
slog := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
45+
fseh := generatecmd.NewFSEventHandler(slog, ".", false, []generator.GenerateOpt{}, false, false, true)
46+
for _, test := range tests {
47+
// The raw files cannot end in .templ because they will cause the generator to fail. Instead,
48+
// we create a tmp file that ends in .templ only for the duration of the test.
49+
rawFile, err := os.Open(test.rawFileName)
50+
if err != nil {
51+
t.Errorf("%s: Failed to open file %s: %v", test.name, test.rawFileName, err)
52+
break
53+
}
54+
file, err := os.CreateTemp("", fmt.Sprintf("*%s.templ", test.rawFileName))
55+
if err != nil {
56+
t.Errorf("%s: Failed to create a tmp file at %s: %v", test.name, file.Name(), err)
57+
break
58+
}
59+
defer os.Remove(file.Name())
60+
if _, err = io.Copy(file, rawFile); err != nil {
61+
t.Errorf("%s: Failed to copy contents from raw file %s to tmp %s: %v", test.name, test.rawFileName, file.Name(), err)
62+
}
63+
64+
event := fsnotify.Event{Name: file.Name(), Op: fsnotify.Write}
65+
_, _, err = fseh.HandleEvent(context.Background(), event)
66+
if err == nil {
67+
t.Errorf("%s: no error was thrown", test.name)
68+
break
69+
}
70+
list, ok := err.(scanner.ErrorList)
71+
for !ok {
72+
err = errors.Unwrap(err)
73+
if err == nil {
74+
t.Errorf("%s: reached end of error wrapping before finding an ErrorList", test.name)
75+
break
76+
} else {
77+
list, ok = err.(scanner.ErrorList)
78+
}
79+
}
80+
if !ok {
81+
break
82+
}
83+
84+
if len(list) != len(test.errorPositions) {
85+
t.Errorf("%s: expected %d errors but got %d", test.name, len(test.errorPositions), len(list))
86+
break
87+
}
88+
for i, err := range list {
89+
test.errorPositions[i].Filename = file.Name()
90+
diff := cmp.Diff(test.errorPositions[i], err.Pos)
91+
if diff != "" {
92+
t.Error(diff)
93+
t.Error("expected:")
94+
t.Error(test.errorPositions[i])
95+
t.Error("actual:")
96+
t.Error(err.Pos)
97+
}
98+
}
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package testeventhandler
2+
3+
func invalid(a: string) string {
4+
return "foo"
5+
}
6+
7+
templ multipleError(a: string) {
8+
<div/>
9+
}
10+
l
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package testeventhandler
2+
3+
templ singleError(a: string) {
4+
<div/>
5+
}

0 commit comments

Comments
 (0)