diff --git a/depaware-main.go b/depaware-main.go index afb419f..e1639a4 100644 --- a/depaware-main.go +++ b/depaware-main.go @@ -13,7 +13,9 @@ // See https://github.com/tailscale/depaware package main -import "github.com/tailscale/depaware/depaware" +import ( + "github.com/tailscale/depaware/depaware" +) func main() { depaware.Main() diff --git a/depaware.txt b/depaware.txt index a9ca4ad..42286a6 100644 --- a/depaware.txt +++ b/depaware.txt @@ -9,7 +9,7 @@ github.com/tailscale/depaware dependencies: (generated by github.com/tailscale/d github.com/tailscale/depaware/depaware from github.com/tailscale/depaware golang.org/x/mod/module from golang.org/x/tools/internal/imports golang.org/x/mod/semver from golang.org/x/mod/module+ - golang.org/x/tools/go/ast/astutil from golang.org/x/tools/internal/imports + golang.org/x/tools/go/ast/astutil from golang.org/x/tools/internal/imports+ golang.org/x/tools/go/gcexportdata from golang.org/x/tools/go/packages golang.org/x/tools/go/packages from github.com/tailscale/depaware/depaware golang.org/x/tools/imports from github.com/tailscale/depaware/depaware @@ -22,21 +22,23 @@ github.com/tailscale/depaware dependencies: (generated by github.com/tailscale/d encoding from encoding/json encoding/base64 from encoding/json encoding/binary from encoding/base64+ - encoding/json from golang.org/x/tools/go/internal/packagesdriver+ + encoding/json from golang.org/x/tools/go/packages+ errors from bufio+ flag from github.com/tailscale/depaware/depaware fmt from encoding/json+ go/ast from go/build+ go/build from golang.org/x/tools/go/internal/gcimporter+ + go/build/constraint from go/build+ go/constant from go/types+ go/doc from go/build - go/format from golang.org/x/tools/internal/imports + go/format from golang.org/x/tools/internal/imports+ go/parser from go/build+ go/printer from go/format+ go/scanner from go/ast+ go/token from go/ast+ go/types from golang.org/x/tools/go/gcexportdata+ io from bufio+ + io/fs from go/build+ io/ioutil from github.com/tailscale/depaware/depaware+ log from github.com/tailscale/depaware/depaware+ math from encoding/binary+ @@ -45,7 +47,7 @@ github.com/tailscale/depaware dependencies: (generated by github.com/tailscale/d math/rand from math/big net/url from text/template os from flag+ - os/exec from go/build+ + os/exec from golang.org/x/tools/go/packages+ path from go/build+ path/filepath from github.com/tailscale/depaware/depaware+ reflect from encoding/binary+ diff --git a/depaware/depaware.go b/depaware/depaware.go index 6c0e2fe..b691548 100644 --- a/depaware/depaware.go +++ b/depaware/depaware.go @@ -29,13 +29,19 @@ import ( ) var ( - check = flag.Bool("check", false, "if true, check whether dependencies match the depaware.txt file") - update = flag.Bool("update", false, "if true, update the depaware.txt file") - osList = flag.String("goos", "linux,darwin,windows", "comma-separated list of GOOS values") + check = flag.Bool("check", false, "if true, check whether dependencies match the depaware.txt file") + update = flag.Bool("update", false, "if true, update the depaware.txt file") + dumpSrc = flag.Bool("dumpsrc", false, "if non-empty, dump the source of package") + osList = flag.String("goos", "linux,darwin,windows", "comma-separated list of GOOS values") ) func Main() { flag.Parse() + if *dumpSrc { + if *check || *update { + log.Fatalf("-check and -update can't be use with -dumpsrc") + } + } if *check && *update { log.Fatalf("-check and -update can't be used together") } @@ -50,6 +56,10 @@ func Main() { } } for i, pkg := range ipaths { + if *dumpSrc { + dumpSource(pkg) + continue + } process(pkg) // If we're printing to stdout, and there are more packages to come, // add an extra newline. diff --git a/depaware/dumpsrc.go b/depaware/dumpsrc.go new file mode 100644 index 0000000..2496e5f --- /dev/null +++ b/depaware/dumpsrc.go @@ -0,0 +1,311 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO: interface types are only used if their itab is used +// TODO: other types are used if ... something? errorsString. pcln table? +// TODO: go:linkname + +package depaware + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "sort" + "strings" + + "go/ast" + "go/format" + "go/printer" + "go/token" + + "github.com/tailscale/depaware/internal/edit" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" +) + +var cfg = &packages.Config{ + Mode: (0 | + packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedModule | + packages.NeedTypes | + packages.NeedSyntax | + 0), +} + +func dumpSource(pkgMain string) { + var w walker + w.walk(pkgMain) +} + +type walker struct { + done map[string]bool + symLive map[string]bool +} + +func (w *walker) dead(sym string) bool { + if !w.symLive[sym] { + log.Printf("DEAD: %q\n", sym) + return true + } + return false +} + +func (w *walker) walk(mainPkg string) { + buildErrc := make(chan error, 1) + go func() { + var err error + w.symLive, err = buildGenLive(mainPkg) + j, _ := json.MarshalIndent(w.symLive, "", "\t") + log.Printf("live: %s", j) + buildErrc <- err + }() + + pkgs, err := packages.Load(cfg, mainPkg) + if err != nil { + log.Fatal(err) + } + + if err := <-buildErrc; err != nil { + log.Fatalf("building: %v", err) + } + + for _, pkg := range pkgs { + w.walkPackage(pkg) + } +} + +func (w *walker) walkPackage(pkg *packages.Package) { + if w.done[pkg.PkgPath] { + return + } + if w.done == nil { + w.done = map[string]bool{} + } + w.done[pkg.PkgPath] = true + + fmt.Printf("\n### PACKAGE %v\n", pkg.PkgPath) + + if len(pkg.Errors) > 0 { + log.Fatalf("errors reading %q: %q", pkg.PkgPath, pkg.Errors) + } + + var imports []*packages.Package + for _, p := range pkg.Imports { + imports = append(imports, p) + } + sort.Slice(imports, func(i, j int) bool { + return imports[i].PkgPath < imports[j].PkgPath + }) + for _, f := range pkg.GoFiles { + fmt.Printf("file.go %q\n", f) + } + for _, f := range pkg.OtherFiles { + fmt.Printf("file.other %q\n", f) + } + for _, p := range imports { + fmt.Printf("import %q => %q\n", pkg.PkgPath, p.PkgPath) + } + fmt.Printf("Fset: %p\n", pkg.Fset) + fmt.Printf("Syntax: %v\n", len(pkg.Syntax)) + fmt.Printf("Modules: %+v\n", pkg.Module) + + for i, f := range pkg.Syntax { + fileName := pkg.GoFiles[i] + + src, err := ioutil.ReadFile(fileName) + if err != nil { + log.Fatal(err) + } + editBuf := edit.NewBuffer(src) + + depth := 0 + post := func(c *astutil.Cursor) bool { + depth-- + return true + } + pre := func(c *astutil.Cursor) bool { + depth++ + n := c.Node() + //indent := strings.Repeat(" ", depth) + //log.Printf("%sNode: %T", indent, n) + switch n := n.(type) { + case *ast.GenDecl: + genDecl := n + + var dels []delRange + for _, spec := range n.Specs { + switch spec := spec.(type) { + case *ast.ImportSpec: + // Nothing (yet?) + case *ast.TypeSpec: + name := typeName(pkg, spec) + // log.Printf("%stype %q", indent, name) + if w.dead(name) { + start, end := offsetRange(pkg.Fset, spec) + dels = append(dels, delRange{start, end}) + } + case *ast.ValueSpec: + // Consts and vars. + } + } + if len(dels) == len(n.Specs) { + // Delete the whole genspec. + start, end := offsetRange(pkg.Fset, genDecl) + editBuf.Delete(start, end) + } else { + for _, del := range dels { + editBuf.Delete(del.start, del.end) + } + } + case *ast.FuncDecl: + name := funcName(pkg, n) + //log.Printf("%sfunc %q comment = %p", indent, name, n.Doc) + if w.dead(name) { + start, end := offsetRange(pkg.Fset, n) + editBuf.Delete(start, end) + return false + } + //log.Printf("%sFunc: %v", indent, name) + } + return true + } + astutil.Apply(f, pre, post) + + src = editBuf.Bytes() + if fmtSrc, err := format.Source(src); err == nil { + src = fmtSrc + } + fmt.Printf("// Source of %s:\n\n%s\n", fileName, src) + } + + for _, p := range imports { + w.walkPackage(p) + } +} + +func pkgSym(pkg *packages.Package) string { + if pkg.Name == "main" { + return "main" + } + return pkg.PkgPath +} + +func typeName(pkg *packages.Package, ts *ast.TypeSpec) string { + return pkgSym(pkg) + "." + ts.Name.Name +} + +func funcName(pkg *packages.Package, fd *ast.FuncDecl) string { + pkgName := pkgSym(pkg) + if fd.Recv != nil { + var buf bytes.Buffer + buf.WriteByte('(') + typ := fd.Recv.List[0].Type + printer.Fprint(&buf, pkg.Fset, typ) + buf.WriteByte(')') + typPart := buf.Bytes() + if typPart[1] != '*' { + typPart = typPart[1 : len(typPart)-1] + } + return fmt.Sprintf("%s.%s.%s", pkgName, typPart, fd.Name.Name) + } + return pkgName + "." + fd.Name.Name +} + +func offset(fset *token.FileSet, pos token.Pos) int { + return fset.PositionFor(pos, false).Offset +} + +func offsetRange(fset *token.FileSet, n ast.Node) (start, end int) { + defer func() { + log.Printf("offSetRange of %T = %v, %v", n, start, end) + }() + startPos, endPos := n.Pos(), n.End() + switch n := n.(type) { + case *ast.FuncDecl: + if n.Doc != nil { + startPos = n.Doc.Pos() + } + case *ast.TypeSpec: + if n.Doc != nil { + startPos = n.Doc.Pos() + } + case *ast.ValueSpec: + if n.Doc != nil { + startPos = n.Doc.Pos() + } + case *ast.GenDecl: + if n.Doc != nil { + startPos = n.Doc.Pos() + } + default: + panic(fmt.Sprintf("unhandled type %T", n)) + } + return offset(fset, startPos), offset(fset, endPos) +} + +type delRange struct{ start, end int } + +func buildGenLive(pkgpath string) (map[string]bool, error) { + tmp, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + defer os.Remove(tmp.Name()) + // Build + { + cmd := exec.Command("go", "build", "-o", tmp.Name(), "-gcflags=all=-N -l", pkgpath) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%v: %s\n%w", cmd, out, err) + } + } + + cmd := exec.Command("go", "tool", "nm", tmp.Name()) + out, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + syms := make(map[string]bool) + scan := bufio.NewScanner(out) + for scan.Scan() { + parts := bytes.Fields(scan.Bytes()) + if len(parts) != 3 { + continue + } + name := string(parts[2]) + if strings.Contains(name, "..") { + // generated algs, inittask, gobytes, anonymous functions + continue + } + if strings.Contains(name, ",") { + // go.itab entry + continue + } + if strings.HasPrefix(name, "$") { + // floating point constant + continue + } + syms[name] = true + } + if scan.Err() != nil { + return nil, err + } + if err := cmd.Wait(); err != nil { + return nil, err + } + return syms, nil +} diff --git a/internal/edit/edit.go b/internal/edit/edit.go new file mode 100644 index 0000000..2d470f4 --- /dev/null +++ b/internal/edit/edit.go @@ -0,0 +1,93 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package edit implements buffered position-based editing of byte slices. +package edit + +import ( + "fmt" + "sort" +) + +// A Buffer is a queue of edits to apply to a given byte slice. +type Buffer struct { + old []byte + q edits +} + +// An edit records a single text modification: change the bytes in [start,end) to new. +type edit struct { + start int + end int + new string +} + +// An edits is a list of edits that is sortable by start offset, breaking ties by end offset. +type edits []edit + +func (x edits) Len() int { return len(x) } +func (x edits) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x edits) Less(i, j int) bool { + if x[i].start != x[j].start { + return x[i].start < x[j].start + } + return x[i].end < x[j].end +} + +// NewBuffer returns a new buffer to accumulate changes to an initial data slice. +// The returned buffer maintains a reference to the data, so the caller must ensure +// the data is not modified until after the Buffer is done being used. +func NewBuffer(data []byte) *Buffer { + return &Buffer{old: data} +} + +func (b *Buffer) Insert(pos int, new string) { + if pos < 0 || pos > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{pos, pos, new}) +} + +func (b *Buffer) Delete(start, end int) { + if end < start || start < 0 || end > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{start, end, ""}) +} + +func (b *Buffer) Replace(start, end int, new string) { + if end < start || start < 0 || end > len(b.old) { + panic("invalid edit position") + } + b.q = append(b.q, edit{start, end, new}) +} + +// Bytes returns a new byte slice containing the original data +// with the queued edits applied. +func (b *Buffer) Bytes() []byte { + // Sort edits by starting position and then by ending position. + // Breaking ties by ending position allows insertions at point x + // to be applied before a replacement of the text at [x, y). + sort.Stable(b.q) + + var new []byte + offset := 0 + for i, e := range b.q { + if e.start < offset { + e0 := b.q[i-1] + panic(fmt.Sprintf("overlapping edits: [%d,%d)->%q, [%d,%d)->%q", e0.start, e0.end, e0.new, e.start, e.end, e.new)) + } + new = append(new, b.old[offset:e.start]...) + offset = e.end + new = append(new, e.new...) + } + new = append(new, b.old[offset:]...) + return new +} + +// String returns a string containing the original data +// with the queued edits applied. +func (b *Buffer) String() string { + return string(b.Bytes()) +} diff --git a/internal/edit/edit_test.go b/internal/edit/edit_test.go new file mode 100644 index 0000000..0e0c564 --- /dev/null +++ b/internal/edit/edit_test.go @@ -0,0 +1,28 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package edit + +import "testing" + +func TestEdit(t *testing.T) { + b := NewBuffer([]byte("0123456789")) + b.Insert(8, ",7½,") + b.Replace(9, 10, "the-end") + b.Insert(10, "!") + b.Insert(4, "3.14,") + b.Insert(4, "π,") + b.Insert(4, "3.15,") + b.Replace(3, 4, "three,") + want := "012three,3.14,π,3.15,4567,7½,8the-end!" + + s := b.String() + if s != want { + t.Errorf("b.String() = %q, want %q", s, want) + } + sb := b.Bytes() + if string(sb) != want { + t.Errorf("b.Bytes() = %q, want %q", sb, want) + } +} diff --git a/test/smallbin/smallbin.go b/test/smallbin/smallbin.go new file mode 100644 index 0000000..00a3a72 --- /dev/null +++ b/test/smallbin/smallbin.go @@ -0,0 +1,54 @@ +package main + +// Comment for main. +// +// Last line. +func main() { + Foo() + + var ft FooType + ft.ValueMethod() + ft.PtrMethod() +} + +// Comment for Foo. +// +// Last line. +func Foo() { + println("Foo") +} + +// Bar is a bar. +func Bar() { + // Unused. +} + +// UnusedType is unused. +type UnusedType struct { + // foo + // bar +} + +type ( + // UnusedFactoredType comment. + UnusedFactoredType struct { + a, + b string + } + // UsedFactoredType is used. + UsedFactoredType int +) + +// Comment on a whole group. +type ( +// Nothing in here anyway. +) + +// FooType is a used type. +type FooType struct { + x int +} + +func (FooType) ValueMethod() {} + +func (*FooType) PtrMethod() {}