1
0
Fork 0
mirror of https://github.com/imjasonh/slight synced 2026-07-07 00:33:20 +00:00

debloat command

This commit is contained in:
Jason Hall 2026-01-12 18:01:45 -05:00
parent c13bd1ca36
commit 3ea5a53de3
4 changed files with 848 additions and 16 deletions

View file

@ -6,6 +6,7 @@ import (
"os"
"github.com/imjasonh/slight/pkg/analysis"
"github.com/imjasonh/slight/pkg/debloat"
"github.com/imjasonh/slight/pkg/report"
)
@ -21,6 +22,11 @@ func main() {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
case "debloat":
if err := runDebloat(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
case "help", "-h", "--help":
printUsage()
default:
@ -38,6 +44,7 @@ Usage:
Commands:
analyze Analyze vendored dependencies for unused symbols
debloat Remove unused code from vendored dependencies
Run 'slight <command> -h' for more information about a command.
`)
@ -88,3 +95,100 @@ Examples:
}
return report.WriteText(os.Stdout, result, *unusedOnly, *verbose)
}
func runDebloat(args []string) error {
fs := flag.NewFlagSet("debloat", flag.ExitOnError)
dryRun := fs.Bool("dry-run", false, "Show what would be removed without making changes")
verbose := fs.Bool("v", false, "Verbose output")
backup := fs.Bool("backup", false, "Create backup of vendor/ before modification")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, `Remove unused code from vendored dependencies.
Usage:
slight debloat [flags] [path]
Flags:
`)
fs.PrintDefaults()
fmt.Fprintf(os.Stderr, `
Examples:
slight debloat .
slight debloat -dry-run ./path/to/module
`)
}
if err := fs.Parse(args); err != nil {
return err
}
path := "."
if fs.NArg() > 0 {
path = fs.Arg(0)
}
// Run debloat
result, err := debloat.Debloat(debloat.Options{
Dir: path,
DryRun: *dryRun,
Verbose: *verbose,
Backup: *backup,
})
if err != nil {
return err
}
// Print result
printDebloatResult(result, *verbose)
return nil
}
func printDebloatResult(result *debloat.Result, verbose bool) {
if result.DryRun {
fmt.Println("Dry run - no changes made")
fmt.Println()
}
// Print warnings
for _, w := range result.Warnings {
fmt.Printf("Warning: %s\n", w)
}
if len(result.Warnings) > 0 {
fmt.Println()
}
if len(result.FilesRemoved) > 0 {
if result.DryRun {
fmt.Printf("Files to remove (%d):\n", len(result.FilesRemoved))
} else {
fmt.Printf("Files removed (%d):\n", len(result.FilesRemoved))
}
if verbose {
for _, f := range result.FilesRemoved {
fmt.Printf(" %s\n", f)
}
}
fmt.Println()
}
if len(result.FilesModified) > 0 {
if result.DryRun {
fmt.Printf("Files to modify (%d):\n", len(result.FilesModified))
} else {
fmt.Printf("Files modified (%d):\n", len(result.FilesModified))
}
if verbose {
for _, f := range result.FilesModified {
fmt.Printf(" %s\n", f)
}
}
fmt.Println()
}
if result.DryRun {
fmt.Printf("Total symbols to remove: %d\n", result.SymbolsRemoved)
} else {
fmt.Printf("Total symbols removed: %d\n", result.SymbolsRemoved)
fmt.Println("Compilation verified successfully")
}
}

420
pkg/debloat/debloat.go Normal file
View file

@ -0,0 +1,420 @@
// Package debloat removes unused code from vendored dependencies.
package debloat
import (
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/imjasonh/slight/pkg/analysis"
"github.com/imjasonh/slight/pkg/report"
)
// Options configures the debloating operation.
type Options struct {
// Dir is the directory to debloat (defaults to ".")
Dir string
// DryRun shows what would be removed without making changes
DryRun bool
// Verbose enables verbose output
Verbose bool
// Backup creates a backup of vendor/ before modification
Backup bool
}
// Result contains the results of a debloat operation.
type Result struct {
// FilesRemoved is a list of files that were completely removed
FilesRemoved []string
// FilesModified is a list of files that had symbols removed
FilesModified []string
// SymbolsRemoved is the count of symbols removed
SymbolsRemoved int
// Warnings contains any warnings generated during debloating
Warnings []string
// DryRun indicates if this was a dry run
DryRun bool
}
// FileRemoval represents a file to be completely removed.
type FileRemoval struct {
Path string
Symbols []report.Symbol
}
// SymbolRemoval represents symbols to be removed from a file.
type SymbolRemoval struct {
Path string
Symbols []report.Symbol
}
// RemovalPlan contains the planned removals.
type RemovalPlan struct {
// FilesToRemove are files where all symbols are unused
FilesToRemove []FileRemoval
// SymbolsToRemove are symbols to remove from partially-used files
SymbolsToRemove []SymbolRemoval
// DirsToRemove are empty directories to remove after file removal
DirsToRemove []string
}
// Debloat performs the debloating operation.
func Debloat(opts Options) (*Result, error) {
if opts.Dir == "" {
opts.Dir = "."
}
// First, run analysis to get used/unused symbols
analysisReport, err := analysis.Analyze(analysis.Options{Dir: opts.Dir})
if err != nil {
return nil, fmt.Errorf("analysis failed: %w", err)
}
result := &Result{
DryRun: opts.DryRun,
Warnings: analysisReport.Warnings,
}
// Build removal plan
plan, err := buildRemovalPlan(opts.Dir, analysisReport)
if err != nil {
return nil, fmt.Errorf("building removal plan: %w", err)
}
if opts.DryRun {
// Just report what would be done
for _, f := range plan.FilesToRemove {
result.FilesRemoved = append(result.FilesRemoved, f.Path)
result.SymbolsRemoved += len(f.Symbols)
}
for _, s := range plan.SymbolsToRemove {
result.FilesModified = append(result.FilesModified, s.Path)
result.SymbolsRemoved += len(s.Symbols)
}
return result, nil
}
// Create backup if requested
if opts.Backup {
if err := createBackup(opts.Dir); err != nil {
return nil, fmt.Errorf("creating backup: %w", err)
}
}
// Execute the removal plan
if err := executePlan(opts.Dir, plan, result); err != nil {
return nil, fmt.Errorf("executing removal plan: %w", err)
}
// Verify compilation
if err := verifyCompilation(opts.Dir); err != nil {
return nil, fmt.Errorf("compilation verification failed (this is a bug in slight): %w", err)
}
return result, nil
}
// buildRemovalPlan creates a plan for what to remove based on the analysis report.
func buildRemovalPlan(dir string, rpt *report.Report) (*RemovalPlan, error) {
plan := &RemovalPlan{}
// Group symbols by file
type fileInfo struct {
usedSymbols []report.Symbol
unusedSymbols []report.Symbol
}
fileSymbols := make(map[string]*fileInfo)
for _, pkg := range rpt.Packages {
for _, sym := range pkg.UsedSymbols {
path := sym.Pos.Filename
if path == "" {
continue
}
if fileSymbols[path] == nil {
fileSymbols[path] = &fileInfo{}
}
fileSymbols[path].usedSymbols = append(fileSymbols[path].usedSymbols, sym)
}
for _, sym := range pkg.UnusedSymbols {
path := sym.Pos.Filename
if path == "" {
continue
}
if fileSymbols[path] == nil {
fileSymbols[path] = &fileInfo{}
}
fileSymbols[path].unusedSymbols = append(fileSymbols[path].unusedSymbols, sym)
}
}
// Determine what to remove
for path, info := range fileSymbols {
// Skip init functions - they're always kept
unusedNonInit := filterNonInit(info.unusedSymbols)
if len(info.usedSymbols) == 0 && len(unusedNonInit) > 0 {
// No used symbols in this file - remove the entire file
plan.FilesToRemove = append(plan.FilesToRemove, FileRemoval{
Path: path,
Symbols: unusedNonInit,
})
} else if len(unusedNonInit) > 0 {
// Some used, some unused - remove individual symbols
plan.SymbolsToRemove = append(plan.SymbolsToRemove, SymbolRemoval{
Path: path,
Symbols: unusedNonInit,
})
}
}
// Sort for deterministic output
sort.Slice(plan.FilesToRemove, func(i, j int) bool {
return plan.FilesToRemove[i].Path < plan.FilesToRemove[j].Path
})
sort.Slice(plan.SymbolsToRemove, func(i, j int) bool {
return plan.SymbolsToRemove[i].Path < plan.SymbolsToRemove[j].Path
})
return plan, nil
}
// filterNonInit returns symbols that are not init functions.
func filterNonInit(symbols []report.Symbol) []report.Symbol {
var result []report.Symbol
for _, sym := range symbols {
if !sym.IsInit {
result = append(result, sym)
}
}
return result
}
// executePlan executes the removal plan.
func executePlan(dir string, plan *RemovalPlan, result *Result) error {
// First, remove individual symbols from partially-used files
for _, sr := range plan.SymbolsToRemove {
if err := removeSymbolsFromFile(sr.Path, sr.Symbols); err != nil {
return fmt.Errorf("removing symbols from %s: %w", sr.Path, err)
}
result.FilesModified = append(result.FilesModified, sr.Path)
result.SymbolsRemoved += len(sr.Symbols)
}
// Then, remove entire files
dirsToCheck := make(map[string]bool)
for _, fr := range plan.FilesToRemove {
if err := os.Remove(fr.Path); err != nil {
return fmt.Errorf("removing file %s: %w", fr.Path, err)
}
result.FilesRemoved = append(result.FilesRemoved, fr.Path)
result.SymbolsRemoved += len(fr.Symbols)
dirsToCheck[filepath.Dir(fr.Path)] = true
}
// Remove empty directories
if err := removeEmptyDirs(dir, dirsToCheck); err != nil {
return fmt.Errorf("removing empty directories: %w", err)
}
return nil
}
// removeSymbolsFromFile removes the specified symbols from a Go source file.
func removeSymbolsFromFile(path string, symbols []report.Symbol) error {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return fmt.Errorf("parsing file: %w", err)
}
// Build a set of symbols to remove by line number
// We use line numbers because symbol names might not be unique (overloaded methods, etc.)
toRemove := make(map[int]bool)
for _, sym := range symbols {
toRemove[sym.Pos.Line] = true
}
// Track which comment groups to remove (by their position)
commentsToRemove := make(map[token.Pos]bool)
// Filter declarations
var newDecls []ast.Decl
for _, decl := range file.Decls {
pos := fset.Position(decl.Pos())
if toRemove[pos.Line] {
// Mark associated doc comment for removal
markDeclComments(decl, commentsToRemove)
continue // Remove this declaration
}
// For GenDecl, we might need to remove individual specs
if genDecl, ok := decl.(*ast.GenDecl); ok {
newSpecs, removedSpecs := filterSpecsWithComments(genDecl.Specs, fset, toRemove)
// Mark comments from removed specs
for _, spec := range removedSpecs {
markSpecComments(spec, commentsToRemove)
}
if len(newSpecs) == 0 {
// Mark the GenDecl's doc comment too
if genDecl.Doc != nil {
commentsToRemove[genDecl.Doc.Pos()] = true
}
continue // Remove entire declaration
}
genDecl.Specs = newSpecs
}
newDecls = append(newDecls, decl)
}
file.Decls = newDecls
// Filter out removed comments
var newComments []*ast.CommentGroup
for _, cg := range file.Comments {
if !commentsToRemove[cg.Pos()] {
newComments = append(newComments, cg)
}
}
file.Comments = newComments
// Write the file back
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer f.Close()
cfg := &printer.Config{
Mode: printer.UseSpaces | printer.TabIndent,
Tabwidth: 8,
}
if err := cfg.Fprint(f, fset, file); err != nil {
return fmt.Errorf("writing file: %w", err)
}
return nil
}
// markDeclComments marks the doc comment of a declaration for removal.
func markDeclComments(decl ast.Decl, commentsToRemove map[token.Pos]bool) {
switch d := decl.(type) {
case *ast.FuncDecl:
if d.Doc != nil {
commentsToRemove[d.Doc.Pos()] = true
}
case *ast.GenDecl:
if d.Doc != nil {
commentsToRemove[d.Doc.Pos()] = true
}
}
}
// markSpecComments marks the doc comment of a spec for removal.
func markSpecComments(spec ast.Spec, commentsToRemove map[token.Pos]bool) {
switch s := spec.(type) {
case *ast.TypeSpec:
if s.Doc != nil {
commentsToRemove[s.Doc.Pos()] = true
}
if s.Comment != nil {
commentsToRemove[s.Comment.Pos()] = true
}
case *ast.ValueSpec:
if s.Doc != nil {
commentsToRemove[s.Doc.Pos()] = true
}
if s.Comment != nil {
commentsToRemove[s.Comment.Pos()] = true
}
}
}
// filterSpecsWithComments filters specs in a GenDecl, returning both kept and removed specs.
func filterSpecsWithComments(specs []ast.Spec, fset *token.FileSet, toRemove map[int]bool) (kept, removed []ast.Spec) {
for _, spec := range specs {
pos := fset.Position(spec.Pos())
if toRemove[pos.Line] {
removed = append(removed, spec)
} else {
kept = append(kept, spec)
}
}
return kept, removed
}
// removeEmptyDirs removes empty directories from vendor.
func removeEmptyDirs(dir string, dirsToCheck map[string]bool) error {
vendorDir := filepath.Join(dir, "vendor")
// Sort directories by depth (deepest first) to remove children before parents
var dirs []string
for d := range dirsToCheck {
dirs = append(dirs, d)
}
sort.Slice(dirs, func(i, j int) bool {
return strings.Count(dirs[i], string(os.PathSeparator)) >
strings.Count(dirs[j], string(os.PathSeparator))
})
for _, d := range dirs {
// Only process directories under vendor
if !strings.HasPrefix(d, vendorDir) {
continue
}
// Walk up the directory tree removing empty directories
for d != vendorDir && strings.HasPrefix(d, vendorDir) {
entries, err := os.ReadDir(d)
if err != nil {
break
}
if len(entries) == 0 {
if err := os.Remove(d); err != nil {
break
}
d = filepath.Dir(d)
} else {
break
}
}
}
return nil
}
// createBackup creates a backup of the vendor directory.
func createBackup(dir string) error {
vendorDir := filepath.Join(dir, "vendor")
backupDir := filepath.Join(dir, "vendor.backup")
// Remove existing backup if present
if err := os.RemoveAll(backupDir); err != nil {
return err
}
// Copy vendor to backup
cmd := exec.Command("cp", "-r", vendorDir, backupDir)
cmd.Dir = dir
return cmd.Run()
}
// verifyCompilation runs go build to verify the code still compiles.
func verifyCompilation(dir string) error {
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOFLAGS=-mod=vendor")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("build failed:\n%s", output)
}
return nil
}

298
pkg/debloat/debloat_test.go Normal file
View file

@ -0,0 +1,298 @@
package debloat
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestDebloatDryRun(t *testing.T) {
// Copy testdata to temp dir
tmpDir := t.TempDir()
if err := copyDir("../../testdata/simple", tmpDir); err != nil {
t.Fatalf("copying testdata: %v", err)
}
// Run debloat in dry-run mode
result, err := Debloat(Options{
Dir: tmpDir,
DryRun: true,
})
if err != nil {
t.Fatalf("debloat failed: %v", err)
}
// Verify dry-run flag is set
if !result.DryRun {
t.Error("expected DryRun to be true")
}
// Verify some symbols would be removed
if result.SymbolsRemoved == 0 {
t.Error("expected some symbols to be marked for removal")
}
// Verify the vendor file still exists (dry-run shouldn't modify)
libFile := filepath.Join(tmpDir, "vendor", "example.com", "lib", "lib.go")
if _, err := os.Stat(libFile); os.IsNotExist(err) {
t.Error("vendor file should still exist after dry-run")
}
// Verify the content is unchanged
content, err := os.ReadFile(libFile)
if err != nil {
t.Fatalf("reading vendor file: %v", err)
}
if !strings.Contains(string(content), "UnusedFunc") {
t.Error("UnusedFunc should still be present after dry-run")
}
}
func TestDebloatRemovesUnusedSymbols(t *testing.T) {
// Copy testdata to temp dir
tmpDir := t.TempDir()
if err := copyDir("../../testdata/simple", tmpDir); err != nil {
t.Fatalf("copying testdata: %v", err)
}
// Run debloat
result, err := Debloat(Options{
Dir: tmpDir,
})
if err != nil {
t.Fatalf("debloat failed: %v", err)
}
// Verify some symbols were removed
if result.SymbolsRemoved == 0 {
t.Error("expected some symbols to be removed")
}
// Verify the vendor file still exists (partially used)
libFile := filepath.Join(tmpDir, "vendor", "example.com", "lib", "lib.go")
if _, err := os.Stat(libFile); os.IsNotExist(err) {
t.Error("vendor file should still exist (has used symbols)")
}
// Verify unused symbols are removed
content, err := os.ReadFile(libFile)
if err != nil {
t.Fatalf("reading vendor file: %v", err)
}
// These should be removed
for _, unused := range []string{"UnusedFunc", "UnusedType", "UnusedConst", "UnusedVar", "UnusedMethod"} {
if strings.Contains(string(content), unused) {
t.Errorf("%s should have been removed", unused)
}
}
// These should remain
for _, used := range []string{"UsedFunc", "UsedType", "UsedConst", "UsedVar", "UsedMethod"} {
if !strings.Contains(string(content), used) {
t.Errorf("%s should still be present", used)
}
}
// Verify the code still compiles
cmd := exec.Command("go", "build", "./...")
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GOFLAGS=-mod=vendor")
if output, err := cmd.CombinedOutput(); err != nil {
t.Errorf("code should still compile after debloating: %v\n%s", err, output)
}
}
func TestDebloatWithBackup(t *testing.T) {
// Copy testdata to temp dir
tmpDir := t.TempDir()
if err := copyDir("../../testdata/simple", tmpDir); err != nil {
t.Fatalf("copying testdata: %v", err)
}
// Run debloat with backup
_, err := Debloat(Options{
Dir: tmpDir,
Backup: true,
})
if err != nil {
t.Fatalf("debloat failed: %v", err)
}
// Verify backup was created
backupDir := filepath.Join(tmpDir, "vendor.backup")
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
t.Error("backup directory should exist")
}
// Verify backup has original content
backupFile := filepath.Join(backupDir, "example.com", "lib", "lib.go")
content, err := os.ReadFile(backupFile)
if err != nil {
t.Fatalf("reading backup file: %v", err)
}
if !strings.Contains(string(content), "UnusedFunc") {
t.Error("backup should contain original UnusedFunc")
}
}
func TestDebloatPreservesInitFunctions(t *testing.T) {
// Create a test module with an init function
tmpDir := t.TempDir()
// Create go.mod
goMod := `module testmod
go 1.21
require example.com/lib v1.0.0
`
if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0o644); err != nil {
t.Fatal(err)
}
// Create main.go that uses the library
mainGo := `package main
import (
"fmt"
"example.com/lib"
)
func main() {
fmt.Println(lib.Value)
}
`
if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainGo), 0o644); err != nil {
t.Fatal(err)
}
// Create vendor directory
vendorDir := filepath.Join(tmpDir, "vendor", "example.com", "lib")
if err := os.MkdirAll(vendorDir, 0o755); err != nil {
t.Fatal(err)
}
// Create vendor/modules.txt
modulesTxt := `# example.com/lib v1.0.0
## explicit
example.com/lib
`
if err := os.WriteFile(filepath.Join(tmpDir, "vendor", "modules.txt"), []byte(modulesTxt), 0o644); err != nil {
t.Fatal(err)
}
// Create lib.go with init function
libGo := `package lib
var Value string
func init() {
Value = "initialized"
}
func UnusedFunc() string {
return "unused"
}
`
if err := os.WriteFile(filepath.Join(vendorDir, "lib.go"), []byte(libGo), 0o644); err != nil {
t.Fatal(err)
}
// Run debloat
result, err := Debloat(Options{
Dir: tmpDir,
})
if err != nil {
t.Fatalf("debloat failed: %v", err)
}
// Verify init function is preserved
content, err := os.ReadFile(filepath.Join(vendorDir, "lib.go"))
if err != nil {
t.Fatalf("reading lib.go: %v", err)
}
if !strings.Contains(string(content), "func init()") {
t.Error("init function should be preserved")
}
// UnusedFunc should be removed
if strings.Contains(string(content), "UnusedFunc") {
t.Error("UnusedFunc should be removed")
}
// Verify it still compiles
cmd := exec.Command("go", "build", "./...")
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GOFLAGS=-mod=vendor")
if output, err := cmd.CombinedOutput(); err != nil {
t.Errorf("code should still compile: %v\n%s", err, output)
}
_ = result // Silence unused variable warning
}
func TestBuildRemovalPlan(t *testing.T) {
// Copy testdata to temp dir
tmpDir := t.TempDir()
if err := copyDir("../../testdata/simple", tmpDir); err != nil {
t.Fatalf("copying testdata: %v", err)
}
// Run analysis
result, err := Debloat(Options{
Dir: tmpDir,
DryRun: true,
})
if err != nil {
t.Fatalf("debloat failed: %v", err)
}
// Should have files to modify (partially used file)
if len(result.FilesModified) == 0 && len(result.FilesRemoved) == 0 {
t.Error("expected some files to be modified or removed")
}
// Since the test file has both used and unused symbols,
// it should be in FilesModified (not completely removed)
hasPartialFile := false
for _, f := range result.FilesModified {
if strings.Contains(f, "lib.go") {
hasPartialFile = true
break
}
}
if !hasPartialFile {
t.Error("expected lib.go to be in FilesModified (partially used)")
}
}
// copyDir recursively copies a directory.
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Calculate destination path
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
// Copy file
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(dstPath, data, info.Mode())
})
}

42
plan.md
View file

@ -72,7 +72,7 @@ Produce a report showing which vendored symbols are used vs unused, with percent
- List of unused symbols with file:line locations
- Library API: return structured data, let caller format
### Package Structure (Milestone 1)
### Package Structure
```
slight/
@ -80,16 +80,21 @@ slight/
│ └── main.go # CLI entry point
├── pkg/
│ ├── analysis/
│ │ ├── analyze.go # Main analysis entry point
│ │ ├── loader.go # Package loading with x/tools/go/packages
│ │ ├── inventory.go # Build symbol inventory from vendor
│ │ ├── usage.go # Analyze usage from main code
│ │ └── closure.go # Transitive closure computation
│ ├── debloat/
│ │ └── debloat.go # Vendor debloating logic
│ ├── report/
│ │ ├── report.go # Report data structures
│ │ ├── text.go # Human-readable output
│ │ └── json.go # JSON output
│ └── scan/
│ └── prescan.go # Build tag detection, cgo/asm detection
├── testdata/
│ └── simple/ # Test fixture with vendored library
├── go.mod
└── go.sum
```
@ -113,42 +118,44 @@ Examples:
## Milestone 2: Vendor Debloating
**Status: COMPLETE** ✓
### Goal
Remove unused code from vendor/ while ensuring compilation still succeeds.
### Implementation Steps
1. **Determine removal candidates**
1. **Determine removal candidates**
- From Milestone 1 analysis, identify:
- Entire files where no symbols are used
- Individual symbols to remove from partially-used files
2. **Handle removal dependencies**
2. **Handle removal dependencies**
- Before removing a symbol, verify nothing else in vendor depends on it
- Handle file-level dependencies (package-level vars, types used by remaining code)
- Be conservative: if unsure, keep the symbol
3. **Remove entire unused files**
3. **Remove entire unused files**
- Delete `.go` files where no exported or unexported symbols are used by remaining code
- Remove empty directories from vendor
4. **Remove individual symbols from files**
4. **Remove individual symbols from files**
- Use `go/ast` and `go/printer` to rewrite files
- Remove:
- Unused functions and methods
- Unused types (if no remaining code references them)
- Unused variables and constants
- Associated doc comments
- Preserve:
- `init()` functions
- Anything needed for compilation of remaining code
- Use `gofmt`-compatible output
5. **Verify compilation**
5. **Verify compilation**
- After modifications, run `go build ./...` on the module
- If build fails, this is a bug in slight - report and rollback changes
- Consider doing this incrementally (remove, verify, continue)
6. **Dry-run mode**
6. **Dry-run mode**
- Show what would be removed without making changes
- Useful for review before actual debloating
@ -171,7 +178,7 @@ Examples:
## Testing Strategy
**Status: PARTIAL** - Unit tests implemented, integration tests pending
**Status: COMPLETE** ✓
### Implemented Tests
@ -199,21 +206,24 @@ Run with `go test ./...`
- `TestPreScanAssembly` - Assembly file detection and error
- `TestPreScanSkipsTestFiles` - Test file exclusion
4. **pkg/debloat/debloat_test.go** - Debloating tests
- `TestDebloatDryRun` - Verifies dry-run mode doesn't modify files
- `TestDebloatRemovesUnusedSymbols` - Verifies unused symbols are removed and used symbols remain
- `TestDebloatWithBackup` - Verifies backup creation
- `TestDebloatPreservesInitFunctions` - Verifies init() functions are preserved
- `TestBuildRemovalPlan` - Verifies removal plan generation
### Test Fixtures
- `testdata/simple/` - Simple module with vendored library containing used and unused symbols
### Pending Tests
### Future Test Improvements
1. **Integration tests using testscript** (for Milestone 2)
- Verify debloating removes expected code
- Verify result still compiles after debloating
2. **Additional test cases to cover**
1. **Additional test cases to cover**
- Embedded types
- Interface satisfaction
- Transitive dependencies within vendor
- Packages with init() functions
- Multiple vendored packages with cross-dependencies
---