From b731ac91061247698d585d7887d9c925aa1bb799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80=20=D0=91=D1=8B=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B2?= Date: Fri, 13 Nov 2026 13:57:40 +0500 Subject: [PATCH] fix(stack): preserve literal empty struct arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Виктор Быстров --- stack/source.go | 20 ++++++++- stack/source_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/stack/source.go b/stack/source.go index 19f029d..f6e66aa 100644 --- a/stack/source.go +++ b/stack/source.go @@ -170,6 +170,11 @@ func name(n ast.Node) string { return t.Value case *ast.Ellipsis: return "..." + case *ast.StructType: + if t.Fields != nil && len(t.Fields.List) == 0 { + return "struct{}" + } + return "" default: return "" } @@ -202,6 +207,8 @@ func fieldToType(f *ast.Field) (string, bool) { return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false case *ast.ChanType: return "chan " + name(arg.Value), false + case *ast.StructType: + return name(arg), false default: // TODO(maruel): Implement anything missing. return "", false @@ -278,7 +285,11 @@ func augmentCall(call *Call, f *ast.FuncDecl) { } types, extra := extractArgumentsType(f) - for i := 0; len(flatArgs) != 0; i++ { + regularArgs := len(types) + if extra { + regularArgs-- + } + for i := 0; len(flatArgs) != 0 || (i < regularArgs && types[i] == "struct{}"); i++ { var t string if i >= len(types) { if !extra { @@ -290,6 +301,11 @@ func augmentCall(call *Call, f *ast.FuncDecl) { } else { t = types[i] } + // Empty structs have a value but do not occupy any stack words. + if t == "struct{}" && i < regularArgs { + call.Args.Processed = append(call.Args.Processed, "struct{}{}") + continue + } var str string switch t { case "float32": @@ -341,7 +357,7 @@ func augmentCall(call *Call, f *ast.FuncDecl) { str = fmt.Sprintf("%s(%s, len=%s)", t, name, lenStr) default: switch { - case strings.HasPrefix(t, "*"): + case strings.HasPrefix(t, "*"), strings.HasPrefix(t, "chan "): str = fmt.Sprintf("%s(%s)", t, popName()) case strings.HasPrefix(t, "[]"): name := popName() diff --git a/stack/source_test.go b/stack/source_test.go index e927a86..6380e4b 100644 --- a/stack/source_test.go +++ b/stack/source_test.go @@ -1699,6 +1699,105 @@ func TestAugmentCall(t *testing.T) { } } +func TestAugmentEmptyStructArguments(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + params string + args []Arg + want []string + }{ + { + name: "empty struct", params: "v struct{}", + args: []Arg{{IsAggregate: true}}, + want: []string{"struct{}{}"}, + }, + { + name: "empty structs beside an integer", params: "a struct{}, n int, b struct{}", + args: []Arg{{IsAggregate: true}, {Value: 42}, {IsAggregate: true}}, + want: []string{"struct{}{}", "42", "struct{}{}"}, + }, + { + name: "legacy empty struct", params: "v struct{}", + want: []string{"struct{}{}"}, + }, + { + name: "legacy empty structs beside an integer", params: "a struct{}, n int, b struct{}", + args: []Arg{{Value: 42}}, + want: []string{"struct{}{}", "42", "struct{}{}"}, + }, + { + name: "channel beside an integer", params: "c chan struct{}, n int", + args: []Arg{{Value: 0}, {Value: 42}}, + want: []string{"chan struct{}(0x0)", "42"}, + }, + { + name: "legacy scalar words", params: "s string", + args: []Arg{{Value: 0x1234}, {Value: 3}}, + want: []string{"string(0x1234, len=3)"}, + }, + { + name: "non-empty anonymous struct", params: "s struct{ N int }", + args: []Arg{{IsAggregate: true, Fields: Args{Values: []Arg{{Value: 42}}}}}, + want: []string{"{0x2a}"}, + }, + { + name: "elided aggregate", params: "s struct{ N int }", + args: []Arg{{IsAggregate: true, Fields: Args{Elided: true}}}, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + f, err := parser.ParseFile(token.NewFileSet(), "args.go", "package main; func f("+test.params+") {}", 0) + if err != nil { + t.Fatal(err) + } + decl, ok := f.Decls[0].(*ast.FuncDecl) + if !ok { + t.Fatal("expected function declaration") + } + call := Call{Args: Args{Values: test.args}} + augmentCall(&call, decl) + if diff := cmp.Diff(test.want, call.Args.Processed); diff != "" { + t.Fatalf("decoded arguments (-want +got):\n%s", diff) + } + }) + } +} + +func TestAugmentEmptyStructPanic(t *testing.T) { + t.Parallel() + main := filepath.Join(t.TempDir(), "main.go") + const src = `package main + +func fail(a struct{}, c chan struct{}, n int, b struct{}) { + panic("ooh") +} + +func main() { + fail(struct{}{}, nil, 42, struct{}{}) +} +` + if err := os.WriteFile(main, []byte(src), 0o600); err != nil { + t.Fatal(err) + } + content := getCrash(t, main, true) + s, _, err := ScanSnapshot(bytes.NewReader(content), &bytes.Buffer{}, defaultOpts()) + if err != nil || s == nil { + t.Fatalf("parse empty struct panic: %v\n%s", err, content) + } + if !s.guessPaths() { + t.Fatal("could not resolve panic source") + } + if err := s.augment(); err != nil { + t.Fatal(err) + } + want := []string{"struct{}{}", "chan struct{}(0x0)", "42", "struct{}{}"} + if diff := cmp.Diff(want, s.Goroutines[0].Stack.Calls[0].Args.Processed); diff != "" { + t.Fatalf("decoded panic arguments (-want +got):\n%s", diff) + } +} + func TestAugmentGenericMethod(t *testing.T) { t.Parallel() main := filepath.Join(t.TempDir(), "main.go")