Skip to content

Include original function names into GopherJS source maps. #1338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/gopherjs/gopherjs/compiler/jsFile"
"github.com/gopherjs/gopherjs/compiler/sources"
"github.com/gopherjs/gopherjs/internal/errorList"
"github.com/gopherjs/gopherjs/internal/sourcemapx"
"github.com/gopherjs/gopherjs/internal/testmain"
log "github.com/sirupsen/logrus"

Expand Down Expand Up @@ -1216,9 +1217,9 @@ func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archi
}
}

// SourceMappingCallback returns a call back for compiler.SourceMapFilter
// SourceMappingCallback returns a callback for [github.com/gopherjs/gopherjs/compiler.SourceMapFilter]
// configured for the current build session.
func (s *Session) SourceMappingCallback(m *sourcemap.Map) func(generatedLine, generatedColumn int, originalPos token.Position) {
func (s *Session) SourceMappingCallback(m *sourcemap.Map) func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
return NewMappingCallback(m, s.xctx.Env().GOROOT, s.xctx.Env().GOPATH, s.options.MapToLocalDisk)
}

Expand All @@ -1233,7 +1234,7 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string)
}
defer codeFile.Close()

sourceMapFilter := &compiler.SourceMapFilter{Writer: codeFile}
sourceMapFilter := &sourcemapx.Filter{Writer: codeFile}
if s.options.CreateMapFile {
m := &sourcemap.Map{File: filepath.Base(pkgObj)}
mapFile, err := os.Create(pkgObj + ".map")
Expand All @@ -1258,27 +1259,33 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string)
}

// NewMappingCallback creates a new callback for source map generation.
func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position) {
return func(generatedLine, generatedColumn int, originalPos token.Position) {
if !originalPos.IsValid() {
m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn})
return
func NewMappingCallback(m *sourcemap.Map, goroot, gopath string, localMap bool) func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
return func(generatedLine, generatedColumn int, originalPos token.Position, originalName string) {
mapping := &sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn}

if originalPos.IsValid() {
file := originalPos.Filename

switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); {
case localMap:
// no-op: keep file as-is
case hasGopathPrefix:
file = filepath.ToSlash(file[prefixLen+4:])
case strings.HasPrefix(file, goroot):
file = filepath.ToSlash(file[len(goroot)+4:])
default:
file = filepath.Base(file)
}
mapping.OriginalFile = file
mapping.OriginalLine = originalPos.Line
mapping.OriginalColumn = originalPos.Column
}

file := originalPos.Filename

switch hasGopathPrefix, prefixLen := hasGopathPrefix(file, gopath); {
case localMap:
// no-op: keep file as-is
case hasGopathPrefix:
file = filepath.ToSlash(file[prefixLen+4:])
case strings.HasPrefix(file, goroot):
file = filepath.ToSlash(file[len(goroot)+4:])
default:
file = filepath.Base(file)
if originalName != "" {
mapping.OriginalName = originalName
}

m.AddMapping(&sourcemap.Mapping{GeneratedLine: generatedLine, GeneratedColumn: generatedColumn, OriginalFile: file, OriginalLine: originalPos.Line, OriginalColumn: originalPos.Column})
m.AddMapping(mapping)
}
}

Expand Down
89 changes: 12 additions & 77 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package compiler

import (
"bytes"
"encoding/binary"
"encoding/gob"
"encoding/json"
"fmt"
Expand All @@ -20,6 +19,7 @@ import (
"github.com/gopherjs/gopherjs/compiler/internal/dce"
"github.com/gopherjs/gopherjs/compiler/linkname"
"github.com/gopherjs/gopherjs/compiler/prelude"
"github.com/gopherjs/gopherjs/internal/sourcemapx"
"golang.org/x/tools/go/gcexportdata"
)

Expand Down Expand Up @@ -108,7 +108,13 @@ func ImportDependencies(archive *Archive, importPkg func(string) (*Archive, erro
return deps, nil
}

func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) error {
type dceInfo struct {
decl *Decl
objectFilter string
methodFilter string
}

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

Expand Down Expand Up @@ -165,9 +171,9 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err
return nil
}

func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.GoLinknameSet, minify bool, w *SourceMapFilter) error {
func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.GoLinknameSet, minify bool, w *sourcemapx.Filter) error {
if w.MappingCallback != nil && pkg.FileSet != nil {
w.fileSet = pkg.FileSet
w.FileSet = pkg.FileSet
}
if _, err := w.Write(pkg.IncJSCode); err != nil {
return err
Expand Down Expand Up @@ -316,77 +322,6 @@ func ReadArchive(importPath string, r io.Reader, srcModTime time.Time, imports m
}

// WriteArchive writes compiled package archive on disk for later reuse.
//
// The passed in buildTime is used to determine if the archive is out-of-date.
// Typically it should be set to the srcModTime or time.Now() but it is exposed for testing purposes.
func WriteArchive(a *Archive, buildTime time.Time, w io.Writer) error {
exportData := new(bytes.Buffer)
if a.Package != nil {
if err := gcexportdata.Write(exportData, nil, a.Package); err != nil {
return fmt.Errorf("failed to write export data: %w", err)
}
}

encodedFileSet := new(bytes.Buffer)
if a.FileSet != nil {
if err := a.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil {
return err
}
}

sa := serializableArchive{
ImportPath: a.ImportPath,
Name: a.Name,
Imports: a.Imports,
ExportData: exportData.Bytes(),
Declarations: a.Declarations,
IncJSCode: a.IncJSCode,
FileSet: encodedFileSet.Bytes(),
Minified: a.Minified,
GoLinknames: a.GoLinknames,
BuildTime: buildTime,
}

return gob.NewEncoder(w).Encode(sa)
}

type SourceMapFilter struct {
Writer io.Writer
MappingCallback func(generatedLine, generatedColumn int, originalPos token.Position)
line int
column int
fileSet *token.FileSet
}

func (f *SourceMapFilter) Write(p []byte) (n int, err error) {
var n2 int
for {
i := bytes.IndexByte(p, '\b')
w := p
if i != -1 {
w = p[:i]
}

n2, err = f.Writer.Write(w)
n += n2
for {
i := bytes.IndexByte(w, '\n')
if i == -1 {
f.column += len(w)
break
}
f.line++
f.column = 0
w = w[i+1:]
}

if err != nil || i == -1 {
return
}
if f.MappingCallback != nil {
f.MappingCallback(f.line+1, f.column, f.fileSet.Position(token.Pos(binary.BigEndian.Uint32(p[i+1:i+5]))))
}
p = p[i+5:]
n += 5
}
func WriteArchive(a *Archive, w io.Writer) error {
return gob.NewEncoder(w).Encode(a)
}
6 changes: 3 additions & 3 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/google/go-cmp/cmp"
"github.com/gopherjs/gopherjs/internal/sourcemapx"
"golang.org/x/tools/go/packages"

"github.com/gopherjs/gopherjs/compiler/internal/dce"
Expand Down Expand Up @@ -846,11 +847,10 @@ func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPa
// the old recursive archive loading that is no longer used since it
// doesn't allow cross package analysis for generings.

buildTime := newTime(5.0)
serialized := map[string][]byte{}
for path, a := range archives {
buf := &bytes.Buffer{}
if err := WriteArchive(a, buildTime, buf); err != nil {
if err := WriteArchive(a, buf); err != nil {
t.Fatalf(`failed to write archive for %s: %v`, path, err)
}
serialized[path] = buf.Bytes()
Expand Down Expand Up @@ -903,7 +903,7 @@ func renderPackage(t *testing.T, archive *Archive, minify bool) string {

buf := &bytes.Buffer{}

if err := WritePkgCode(archive, selection, linkname.GoLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil {
if err := WritePkgCode(archive, selection, linkname.GoLinknameSet{}, minify, &sourcemapx.Filter{Writer: buf}); err != nil {
t.Fatal(err)
}

Expand Down
19 changes: 11 additions & 8 deletions compiler/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/gopherjs/gopherjs/compiler/internal/analysis"
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
"github.com/gopherjs/gopherjs/compiler/typesutil"
"github.com/gopherjs/gopherjs/internal/sourcemapx"
)

// nestedFunctionContext creates a new nested context for a function corresponding
Expand Down Expand Up @@ -60,11 +61,13 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep
// Synthesize an identifier by which the function may reference itself. Since
// it appears in the stack trace, it's useful to include the receiver type in
// it.
funcRef := o.Name()
if recvType := typesutil.RecvType(sig); recvType != nil {
funcRef = recvType.Obj().Name() + midDot + funcRef
funcRef := strings.ReplaceAll(o.Name(), ".", midDot)
c.funcRef = sourcemapx.Identifier{
Name: c.newVariable(funcRef, true /*pkgLevel*/),
// o.FullName() decorates pointer receivers as `(*T).method`, we want simply `T.method`.
OriginalName: strings.NewReplacer("(", "", ")", "", "*", "").Replace(o.FullName()),
OriginalPos: o.Pos(),
}
c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/)

return c
}
Expand Down Expand Up @@ -264,7 +267,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,

fc.translateStmtList(body.List)
if len(fc.Flattened) != 0 && !astutil.EndsWithReturn(body.List) {
fc.translateStmt(&ast.ReturnStmt{}, nil)
fc.translateStmt(&ast.ReturnStmt{Return: body.Rbrace}, nil)
}
}))

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

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

fc.pkgCtx.escapingVars = prevEV

return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
return fmt.Sprintf("%sfunction %s(%s) {\n%s%s}", fc.funcRef.EncodeHint(), fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
}
5 changes: 4 additions & 1 deletion compiler/natives/src/net/http/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ package http_test
import "testing"

func TestTimeoutHandlerSuperfluousLogs(t *testing.T) {
t.Skip("https://github.com/gopherjs/gopherjs/issues/1085")
// The test expects nested anonymous functions to be named "Foo.func1.2",
// bug GopherJS generates "Foo.func1.func2". Otherwise the test works as
// expected.
t.Skip("GopherJS uses different synthetic function names.")
}
33 changes: 0 additions & 33 deletions compiler/natives/src/reflect/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package reflect

import (
"errors"
"runtime"
"strconv"
"unsafe"

Expand Down Expand Up @@ -1774,38 +1773,6 @@ func stringsHasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

func valueMethodName() string {
var pc [5]uintptr
n := runtime.Callers(1, pc[:])
frames := runtime.CallersFrames(pc[:n])
valueTyp := TypeOf(Value{})
var frame runtime.Frame
for more := true; more; {
frame, more = frames.Next()
name := frame.Function
// Function name extracted from the call stack can be different from
// vanilla Go, so is not prefixed by "reflect.Value." as needed by the original.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.19.13:src/reflect/value.go;l=173-191
// This workaround may become obsolete after
// https://github.com/gopherjs/gopherjs/issues/1085 is resolved.

methodName := name
if idx := stringsLastIndex(name, '.'); idx >= 0 {
methodName = name[idx+1:]
}

// Since function name in the call stack doesn't contain receiver name,
// we are looking for the first exported function name that matches a
// known Value method.
if _, ok := valueTyp.MethodByName(methodName); ok {
if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' {
return `reflect.Value.` + methodName
}
}
}
return "unknown method"
}

func verifyNotInHeapPtr(p uintptr) bool {
// Go runtime uses this method to make sure that a uintptr won't crash GC if
// interpreted as a heap pointer. This is not relevant for GopherJS, so we can
Expand Down
20 changes: 0 additions & 20 deletions compiler/natives/src/reflect/reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,23 +298,3 @@ func TestIssue50208(t *testing.T) {
func TestStructOfTooLarge(t *testing.T) {
t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.")
}

func TestSetLenCap(t *testing.T) {
t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
}

func TestSetPanic(t *testing.T) {
t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
}

func TestCallPanic(t *testing.T) {
t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
}

func TestValuePanic(t *testing.T) {
t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
}

func TestSetIter(t *testing.T) {
t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
}
3 changes: 2 additions & 1 deletion compiler/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/gopherjs/gopherjs/compiler/sources"
"github.com/gopherjs/gopherjs/compiler/typesutil"
"github.com/gopherjs/gopherjs/internal/errorList"
"github.com/gopherjs/gopherjs/internal/sourcemapx"
)

// pkgContext maintains compiler context for a specific package.
Expand Down Expand Up @@ -60,7 +61,7 @@ type funcContext struct {
// "function" keyword in the generated code). This identifier can be used
// within the function scope to reference the function object. It will also
// appear in the stack trace.
funcRef string
funcRef sourcemapx.Identifier
// Surrounding package context.
pkgCtx *pkgContext
// Function context, surrounding this function definition. For package-level
Expand Down
Loading
Loading