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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ structalign [flags] [packages]
-summary in diff mode, print a one-line summary after the diffs
-sort present results largest-first (diff: by bytes saved;
inspect: by struct size)
-threshold int in diff mode, only show structs that save at least N bytes
(default 0; negatives treated as 0)

-type string only consider named structs matching these comma-separated
glob patterns (e.g. "*Request,Config"); empty means all
Expand Down
13 changes: 13 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type options struct {
skipCachePadded bool
summary bool
sort bool
threshold int
}

// savings is the absolute bytes a finding saves, or 0 when sizes are unknown or
Expand Down Expand Up @@ -108,6 +109,7 @@ func (a *App) Run(args []string) int {
fs.BoolVar(&opt.skipCachePadded, "skip-cache-padded", false, "skip structs containing a golang.org/x/sys/cpu.CacheLinePad field")
fs.BoolVar(&opt.summary, "summary", false, "in diff mode, print a one-line summary after the diffs")
fs.BoolVar(&opt.sort, "sort", false, "present results largest-first (diff: by bytes saved; inspect: by struct size)")
fs.IntVar(&opt.threshold, "threshold", 0, "in diff mode, only show structs that save at least this many bytes")
fs.Usage = func() {
fmt.Fprintf(a.Stderr, "structalign: print field-aligned struct reorderings (no file changes)\n\n")
fmt.Fprintf(a.Stderr, "usage: structalign [flags] [packages]\n\n")
Expand Down Expand Up @@ -197,6 +199,17 @@ func (a *App) Run(args []string) int {
}
}

if !opt.inspect && opt.threshold > 0 {
min := int64(opt.threshold)
kept := allFindings[:0]
for _, f := range allFindings {
if savings(f) >= min {
kept = append(kept, f)
}
}
allFindings = kept
}

if opt.sort {
if opt.inspect {
sort.SliceStable(allLayouts, func(i, j int) bool {
Expand Down
55 changes: 55 additions & 0 deletions internal/app/threshold_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package app_test

import (
"bytes"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

// These reuse sortApp / sortSrc from sort_test.go, where Small saves 8 bytes
// (24->16) and Big saves 16 (40->24).

func TestRunThresholdFiltersSmallSavings(t *testing.T) {
var out, errb bytes.Buffer
a := sortApp(t, &out, &errb)
// 16 is inclusive: keeps Big (saves 16), drops Small (saves 8).
code := a.Run([]string{"-threshold=16", "pkg"})
assert.Equal(t, 1, code)
s := out.String()
assert.Contains(t, s, "Big")
assert.NotContains(t, s, "Small")
}

func TestRunThresholdZeroShowsAll(t *testing.T) {
var out, errb bytes.Buffer
a := sortApp(t, &out, &errb)
_ = a.Run([]string{"-threshold=0", "pkg"})
s := out.String()
assert.Contains(t, s, "Big")
assert.Contains(t, s, "Small")
}

func TestRunThresholdNegativeIsZero(t *testing.T) {
var out, errb bytes.Buffer
a := sortApp(t, &out, &errb)
_ = a.Run([]string{"-threshold=-5", "pkg"})
assert.Contains(t, out.String(), "Small", "a negative threshold behaves like 0 (no filtering)")
}

func TestRunThresholdExitsZeroWhenAllFiltered(t *testing.T) {
var out, errb bytes.Buffer
a := sortApp(t, &out, &errb)
code := a.Run([]string{"-threshold=10000", "pkg"})
assert.Equal(t, 0, code, "all filtered out => no findings => exit 0")
assert.Equal(t, "", strings.TrimSpace(out.String()))
}

func TestRunThresholdSummaryReflectsFiltered(t *testing.T) {
var out, errb bytes.Buffer
a := sortApp(t, &out, &errb)
_ = a.Run([]string{"-threshold=16", "-summary", "pkg"})
assert.Contains(t, out.String(), "Summary: 1 struct affected, 16 bytes saved",
"summary counts only the structs that pass the threshold")
}
Loading