Skip to content
Merged
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
44 changes: 28 additions & 16 deletions godump.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ const (

// Default configuration values for the Dumper.
const (
defaultMaxDepth = 15
defaultMaxItems = 100
defaultMaxStringLen = 100000
defaultMaxStackDepth = 10
initialCallerSkip = 2
defaultDisableStringer = false
defaultMaxDepth = 15
defaultMaxItems = 100
defaultMaxStringLen = 100000
defaultMaxStackDepth = 10
initialCallerSkip = 2
)

// defaultDumper is the default Dumper instance used by Dump and DumpStr functions.
Expand Down Expand Up @@ -90,6 +91,7 @@ type Dumper struct {
maxStringLen int
writer io.Writer
skippedStackFrames int
disableStringer bool

// callerFn is used to get the caller information.
// It defaults to [runtime.Caller], it is here to be overridden for testing purposes.
Expand Down Expand Up @@ -156,16 +158,26 @@ func WithSkipStackFrames(n int) Option {
}
}

// WithDisableStringer will determine if the stringer value for types that
// implement the stringer interface should be render instead of the actual type.
func WithDisableStringer(b bool) Option {
return func(d *Dumper) *Dumper {
d.disableStringer = b
return d
}
}

// NewDumper creates a new Dumper with the given options applied.
// Defaults are used for any setting not overridden.
func NewDumper(opts ...Option) *Dumper {
d := &Dumper{
maxDepth: defaultMaxDepth,
maxItems: defaultMaxItems,
maxStringLen: defaultMaxStringLen,
writer: os.Stdout,
colorizer: nil, // ensure no detection is made if we don't need it
callerFn: runtime.Caller,
maxDepth: defaultMaxDepth,
maxItems: defaultMaxItems,
maxStringLen: defaultMaxStringLen,
disableStringer: defaultDisableStringer,
writer: os.Stdout,
colorizer: nil, // ensure no detection is made if we don't need it
callerFn: runtime.Caller,
}
for _, opt := range opts {
d = opt(d)
Expand Down Expand Up @@ -472,11 +484,7 @@ func (d *Dumper) printValue(w io.Writer, v reflect.Value, indent int, visited ma
}
indentPrint(w, indent+1, d.colorize(colorYellow, symbol)+field.Name)
fmt.Fprint(w, " => ")
if s := d.asStringer(fieldVal); s != "" {
fmt.Fprint(w, s)
} else {
d.printValue(w, fieldVal, indent+1, visited)
}
d.printValue(w, fieldVal, indent+1, visited)
fmt.Fprintln(w)
}
indentPrint(w, indent, "")
Expand Down Expand Up @@ -553,6 +561,10 @@ func (d *Dumper) printValue(w io.Writer, v reflect.Value, indent int, visited ma

// asStringer checks if the value implements fmt.Stringer and returns its string representation.
func (d *Dumper) asStringer(v reflect.Value) string {
if d.disableStringer {
return ""
}
Copy link
Member

@Akkadius Akkadius Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to return blank? I would think we would at least want the following to return the raw "non-stringer" value from the struct ? I could be wrong here

	if !val.CanInterface() {
		val = forceExported(val)
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting, I'm not sure I follow when and what that is supposed to work 🤔

The test for this is on a private field in a private struct and it seems to work as expected by returning the empty string to continue the processing in printValue to figure out the type.

https://github.com/bombsimon/godump/blob/781f048e52e61a9ef387b4e81c4adfe328caff51/godump_test.go#L1051-L1061

I'll do some more testing but feel free to add an example on when this would be needed if you know how to! Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't get too much time, but quickly glancing at the code, it looks like the need for this forced export is so we can call val.Interface().(fmt.Stringer) on private fields. But we won't end up in this path if we're not looking for strings so we don't have a use of what's returned from forceExported.

We do something similar at printValue for struct fields which I think is what you're referring to to show private fields. But it's not important that we force a value that can call Interface() here either because we don't do that.

I don't think we need to do any change at least to get expected behavior. I updated the test to ensure we see the private field and I also removed one place where we call asStringer and look for the empty string which can be done recursively instead.


val := v
if !val.CanInterface() {
val = forceExported(val)
Expand Down
12 changes: 11 additions & 1 deletion godump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,6 @@ func TestTheKitchenSink(t *testing.T) {
// Ensure no panic occurred and a sane dump was produced
assert.Contains(t, out, "#") // loosest
assert.Contains(t, out, "Everything") // middle-ground

}

func TestForceExportedFallback(t *testing.T) {
Expand Down Expand Up @@ -1047,5 +1046,16 @@ func TestDumpJSON(t *testing.T) {

assert.Equal(t, []any{"foo", float64(123), true}, got)
})
}

func TestDisableStringer(t *testing.T) {
data := hidden{secret: "not so secret"}

d := newDumperT(t, WithDisableStringer(true))
v := d.DumpStr(data)
require.Contains(t, v, `-secret => "not so secret"`)

d = newDumperT(t)
v = d.DumpStr(data)
assert.Contains(t, v, `-secret => 👻 hidden stringer`)
}