Skip to content

Commit 66dcffd

Browse files
committed
Include original function names into source maps.
The main change in this commit is an ability to use identifier name mapping, which we use to show original function names in the source map. This addresses the long-standing #1085, where GopherJS call stacks were somewhat difficult to interpret due to function name mangling, especially in minified form. Now we emit an additional source map hit with the original function name, which Node is able to pick up. While at it, I moved source map hinting logic into its own package with tests and added some documentation on how it works. Now it should be easy to extend this mechanism for even richer source maps if we want to.
1 parent 1ebb325 commit 66dcffd

14 files changed

+617
-78
lines changed

build/build.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/fsnotify/fsnotify"
2727
"github.com/gopherjs/gopherjs/compiler"
2828
"github.com/gopherjs/gopherjs/compiler/astutil"
29+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
2930
log "github.com/sirupsen/logrus"
3031

3132
"github.com/neelance/sourcemap"
@@ -1041,7 +1042,7 @@ func (s *Session) ImportResolverFor(pkg *PackageData) func(string) (*compiler.Ar
10411042

10421043
// SourceMappingCallback returns a call back for compiler.SourceMapFilter
10431044
// configured for the current build session.
1044-
func (s *Session) SourceMappingCallback(m *sourcemap.Map) func(generatedLine, generatedColumn int, originalPos token.Position) {
1045+
func (s *Session) SourceMappingCallback(m *sourcemap.Map) func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
10451046
return NewMappingCallback(m, s.xctx.Env().GOROOT, s.xctx.Env().GOPATH, s.options.MapToLocalDisk)
10461047
}
10471048

@@ -1056,7 +1057,7 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string)
10561057
}
10571058
defer codeFile.Close()
10581059

1059-
sourceMapFilter := &compiler.SourceMapFilter{Writer: codeFile}
1060+
sourceMapFilter := &sourcemapx.Filter{Writer: codeFile}
10601061
if s.options.CreateMapFile {
10611062
m := &sourcemap.Map{File: filepath.Base(pkgObj)}
10621063
mapFile, err := os.Create(pkgObj + ".map")
@@ -1087,27 +1088,33 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string)
10871088
}
10881089

10891090
// NewMappingCallback creates a new callback for source map generation.
1090-
func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position) {
1091-
return func(generatedLine, generatedColumn int, originalPos token.Position) {
1092-
if !originalPos.IsValid() {
1093-
m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn})
1094-
return
1091+
func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
1092+
return func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
1093+
mapping := &sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn}
1094+
1095+
if originalPos.IsValid() {
1096+
file := originalPos.Filename
1097+
1098+
switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); {
1099+
case localMap:
1100+
// no-op: keep file as-is
1101+
case hasGopathPrefix:
1102+
file = filepath.ToSlash(file[prefixLen+4:])
1103+
case strings.HasPrefix(file, goroot):
1104+
file = filepath.ToSlash(file[len(goroot)+4:])
1105+
default:
1106+
file = filepath.Base(file)
1107+
}
1108+
mapping.OriginalFile = file
1109+
mapping.OriginalLine = originalPos.Line
1110+
mapping.OriginalColumn = originalPos.Column
10951111
}
10961112

1097-
file := originalPos.Filename
1098-
1099-
switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); {
1100-
case localMap:
1101-
// no-op: keep file as-is
1102-
case hasGopathPrefix:
1103-
file = filepath.ToSlash(file[prefixLen+4:])
1104-
case strings.HasPrefix(file, goroot):
1105-
file = filepath.ToSlash(file[len(goroot)+4:])
1106-
default:
1107-
file = filepath.Base(file)
1113+
if originalName != "" {
1114+
mapping.OriginalName = originalName
11081115
}
11091116

1110-
m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn, OriginalFile: file, OriginalLine: originalPos.Line, OriginalColumn: originalPos.Column})
1117+
m.AddMapping(mapping)
11111118
}
11121119
}
11131120

compiler/compiler.go

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package compiler
77

88
import (
99
"bytes"
10-
"encoding/binary"
1110
"encoding/gob"
1211
"encoding/json"
1312
"fmt"
@@ -18,6 +17,7 @@ import (
1817
"time"
1918

2019
"github.com/gopherjs/gopherjs/compiler/prelude"
20+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
2121
"golang.org/x/tools/go/gcexportdata"
2222
)
2323

@@ -131,7 +131,7 @@ type dceInfo struct {
131131
methodFilter string
132132
}
133133

134-
func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) error {
134+
func WriteProgramCode(pkgs []*Archive, w *sourcemapx.Filter, goVersion string) error {
135135
mainPkg := pkgs[len(pkgs)-1]
136136
minify := mainPkg.Minified
137137

@@ -228,10 +228,10 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err
228228
return nil
229229
}
230230

231-
func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameSet, minify bool, w *SourceMapFilter) error {
231+
func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameSet, minify bool, w *sourcemapx.Filter) error {
232232
if w.MappingCallback != nil && pkg.FileSet != nil {
233-
w.fileSet = token.NewFileSet()
234-
if err := w.fileSet.Read(json.NewDecoder(bytes.NewReader(pkg.FileSet)).Decode); err != nil {
233+
w.FileSet = token.NewFileSet()
234+
if err := w.FileSet.Read(json.NewDecoder(bytes.NewReader(pkg.FileSet)).Decode); err != nil {
235235
panic(err)
236236
}
237237
}
@@ -336,44 +336,3 @@ func ReadArchive(path string, r io.Reader) (*Archive, error) {
336336
func WriteArchive(a *Archive, w io.Writer) error {
337337
return gob.NewEncoder(w).Encode(a)
338338
}
339-
340-
type SourceMapFilter struct {
341-
Writer io.Writer
342-
MappingCallback func(generatedLine, generatedColumn int, originalPos token.Position)
343-
line int
344-
column int
345-
fileSet *token.FileSet
346-
}
347-
348-
func (f *SourceMapFilter) Write(p []byte) (n int, err error) {
349-
var n2 int
350-
for {
351-
i := bytes.IndexByte(p, '\b')
352-
w := p
353-
if i != -1 {
354-
w = p[:i]
355-
}
356-
357-
n2, err = f.Writer.Write(w)
358-
n += n2
359-
for {
360-
i := bytes.IndexByte(w, '\n')
361-
if i == -1 {
362-
f.column += len(w)
363-
break
364-
}
365-
f.line++
366-
f.column = 0
367-
w = w[i+1:]
368-
}
369-
370-
if err != nil || i == -1 {
371-
return
372-
}
373-
if f.MappingCallback != nil {
374-
f.MappingCallback(f.line+1, f.column, f.fileSet.Position(token.Pos(binary.BigEndian.Uint32(p[i+1:i+5]))))
375-
}
376-
p = p[i+5:]
377-
n += 5
378-
}
379-
}

compiler/compiler_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111

1212
"github.com/google/go-cmp/cmp"
13+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
1314
"golang.org/x/tools/go/loader"
1415
)
1516

@@ -138,7 +139,7 @@ func renderPackage(archive *Archive) ([]byte, error) {
138139

139140
buf := &bytes.Buffer{}
140141

141-
if err := WritePkgCode(archive, selection, goLinknameSet{}, false, &SourceMapFilter{Writer: buf}); err != nil {
142+
if err := WritePkgCode(archive, selection, goLinknameSet{}, false, &sourcemapx.Filter{Writer: buf}); err != nil {
142143
return nil, err
143144
}
144145

compiler/functions.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/gopherjs/gopherjs/compiler/astutil"
1717
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
1818
"github.com/gopherjs/gopherjs/compiler/typesutil"
19+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
1920
)
2021

2122
// nestedFunctionContext creates a new nested context for a function corresponding
@@ -64,7 +65,11 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep
6465
if recvType := typesutil.RecvType(sig); recvType != nil {
6566
funcRef = recvType.Obj().Name() + midDot + funcRef
6667
}
67-
c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/)
68+
c.funcRef = sourcemapx.Identifier{
69+
Name: c.newVariable(funcRef, true /*pkgLevel*/),
70+
OriginalName: o.FullName(),
71+
OriginalPos: o.Pos(),
72+
}
6873

6974
return c
7075
}
@@ -299,11 +304,11 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,
299304
localVars = append(localVars, "$r")
300305
// funcRef identifies the function object itself, so it doesn't need to be saved
301306
// or restored.
302-
localVars = removeMatching(localVars, fc.funcRef)
307+
localVars = removeMatching(localVars, fc.funcRef.Name)
303308
// If a blocking function is being resumed, initialize local variables from the saved context.
304309
localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(args, ", "))
305310
// If the function gets blocked, save local variables for future.
306-
saveContext := fmt.Sprintf("var $f = {$blk: "+fc.funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", "))
311+
saveContext := fmt.Sprintf("var $f = {$blk: %s, $c: true, $r, %s};", fc.funcRef, strings.Join(fc.localVars, ", "))
307312

308313
suffix = " " + saveContext + "return $f;" + suffix
309314
} else if len(fc.localVars) > 0 {
@@ -351,5 +356,5 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,
351356

352357
fc.pkgCtx.escapingVars = prevEV
353358

354-
return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
359+
return fmt.Sprintf("%sfunction %s(%s) {\n%s%s}", fc.funcRef.EncodeHint(), fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
355360
}

compiler/package.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
1515
"github.com/gopherjs/gopherjs/compiler/typesutil"
1616
"github.com/gopherjs/gopherjs/internal/experiments"
17+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
1718
"golang.org/x/tools/go/gcexportdata"
1819
"golang.org/x/tools/go/types/typeutil"
1920
)
@@ -61,7 +62,7 @@ type funcContext struct {
6162
// "function" keyword in the generated code). This identifier can be used
6263
// within the function scope to reference the function object. It will also
6364
// appear in the stack trace.
64-
funcRef string
65+
funcRef sourcemapx.Identifier
6566
// Surrounding package context.
6667
pkgCtx *pkgContext
6768
// Function context, surrounding this function definition. For package-level

compiler/utils.go

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

33
import (
44
"bytes"
5-
"encoding/binary"
65
"errors"
76
"fmt"
87
"go/ast"
@@ -21,6 +20,7 @@ import (
2120
"github.com/gopherjs/gopherjs/compiler/analysis"
2221
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
2322
"github.com/gopherjs/gopherjs/compiler/typesutil"
23+
"github.com/gopherjs/gopherjs/internal/sourcemapx"
2424
)
2525

2626
// We use this character as a separator in synthetic identifiers instead of a
@@ -71,8 +71,13 @@ func (fc *funcContext) SetPos(pos token.Pos) {
7171
func (fc *funcContext) writePos() {
7272
if fc.posAvailable {
7373
fc.posAvailable = false
74-
fc.Write([]byte{'\b'})
75-
binary.Write(fc, binary.BigEndian, uint32(fc.pos))
74+
h := sourcemapx.Hint{}
75+
if err := h.Pack(fc.pos); err != nil {
76+
panic(bailout(fmt.Errorf("failed to pack source map position: %w", err)))
77+
}
78+
if _, err := h.WriteTo(fc); err != nil {
79+
panic(bailout(fmt.Errorf("failed to write source map hint: %w", err)))
80+
}
7681
}
7782
}
7883

@@ -868,7 +873,7 @@ func getJsTag(tag string) string {
868873
}
869874

870875
func needsSpace(c byte) bool {
871-
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$'
876+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$' || c == '\b'
872877
}
873878

874879
func removeWhitespace(b []byte, minify bool) []byte {
@@ -881,8 +886,9 @@ func removeWhitespace(b []byte, minify bool) []byte {
881886
for len(b) > 0 {
882887
switch b[0] {
883888
case '\b':
884-
out = append(out, b[:5]...)
885-
b = b[5:]
889+
_, length := sourcemapx.ReadHint(b)
890+
out = append(out, b[:length]...)
891+
b = b[length:]
886892
continue
887893
case ' ', '\t', '\n':
888894
if (!needsSpace(previous) || !needsSpace(b[1])) && !(previous == '-' && b[1] == '-') {

internal/sourcemapx/doc.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Package sourcemapx contains utilities for passing source map information
2+
// around, intended to work with github.com/neelance/sourcemap.
3+
//
4+
// GopherJS code generator outputs hints about correspondence between the
5+
// generated code and original sources inline. Such hints are marked by the
6+
// special `\b` (0x08) magic byte, followed by a variable-length sequence of
7+
// bytes, which can be extracted from the byte slice using ReadHint() function.
8+
//
9+
// '\b' was chosen as a magic symbol because it would never occur unescaped in
10+
// the generated code, other than when explicitly inserted by the source mapping
11+
// hint. See Hint type documentation for the details of the encoded format.
12+
//
13+
// The hinting mechanism is designed to be extensible, the Hint type able to
14+
// wrap different types containing different information:
15+
//
16+
// - go/token.Pos indicates position in the original source the current
17+
// location in the generated code corresponds to.
18+
// - Identifier maps a JS identifier to the original Go identifier it
19+
// represents.
20+
//
21+
// More types may be added in future if necessary.
22+
//
23+
// Filter type is used to extract the hints from the written code stream and
24+
// pass them into source map generator. It also ensures that the encoded inline
25+
// hints don't make it into the final output, since they are not valid JS.
26+
package sourcemapx

internal/sourcemapx/filter.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package sourcemapx
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/token"
7+
"io"
8+
)
9+
10+
// Filter implements io.Writer which extracts source map hints from the written
11+
// stream and passed them to the MappingCallback if it's not nil. Encoded hints
12+
// are always filtered out of the output stream.
13+
type Filter struct {
14+
Writer io.Writer
15+
FileSet *token.FileSet
16+
MappingCallback func(generatedLine, generatedColumn int, originalPos token.Position, originalName string)
17+
18+
line int
19+
column int
20+
}
21+
22+
func (f *Filter) Write(p []byte) (n int, err error) {
23+
var n2 int
24+
for {
25+
i := FindHint(p)
26+
w := p
27+
if i != -1 {
28+
w = p[:i]
29+
}
30+
31+
n2, err = f.Writer.Write(w)
32+
n += n2
33+
for {
34+
i := bytes.IndexByte(w, '\n')
35+
if i == -1 {
36+
f.column += len(w)
37+
break
38+
}
39+
f.line++
40+
f.column = 0
41+
w = w[i+1:]
42+
}
43+
44+
if err != nil || i == -1 {
45+
return
46+
}
47+
h, length := ReadHint(p[i:])
48+
if f.MappingCallback != nil {
49+
value, err := h.Unpack()
50+
if err != nil {
51+
panic(fmt.Errorf("failed to unpack source map hint: %w", err))
52+
}
53+
switch value := value.(type) {
54+
case token.Pos:
55+
f.MappingCallback(f.line+1, f.column, f.FileSet.Position(value), "")
56+
case Identifier:
57+
f.MappingCallback(f.line+1, f.column, f.FileSet.Position(value.OriginalPos), value.OriginalName)
58+
default:
59+
panic(fmt.Errorf("unexpected source map hint type: %T", value))
60+
}
61+
}
62+
p = p[i+length:]
63+
n += length
64+
}
65+
}

0 commit comments

Comments
 (0)