diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6e7a071 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +jobs: + checkout: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - uses: actions/upload-artifact@v2 + with: + name: src + path: . + lint: + needs: checkout + runs-on: ubuntu-18.04 + steps: + - uses: actions/download-artifact@v2 + with: + name: src + - run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.26.0 + - run: bin/golangci-lint run + test: + needs: checkout + runs-on: ubuntu-18.04 + steps: + - uses: actions/download-artifact@v2 + with: + name: src + - uses: actions/setup-go@v2 + with: + go-version: "1.13" + - run: go test -v ./... +name: main +on: + pull_request: + branches: + - master diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..f84ab58 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,7 @@ +issues: + exclude-use-default: false + +linters: + enable: + - goimports + - golint diff --git a/README.md b/README.md index 2f1938c..598e2c9 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ Ever wanted a consistent ordering for declarations in your Go code? With `goarrange`, you can automatically follow the conventions of GoDoc! Constants come first, followed by variables, functions and types with their associated constants, -variables, functions and methods. Within each of these categories, exported declarations precede unexported ones. +variables, functions and methods. Within each of these categories, exported declarations precede unexported ones. Lastly +`goarrange` enforces an alphabetic ordering. ## Installation -Installing `goarrange` requires [Go](https://golang.org) 1.13+. - ```sh -$ go install github.com/jdeflander/goarrange +$ go get github.com/jdeflander/goarrange ``` ## Usage diff --git a/go.mod b/go.mod index 1e6e7b6..13d9bbc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/jdeflander/goarrange go 1.13 + +require github.com/google/go-cmp v0.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4430646 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/help.go b/help.go deleted file mode 100644 index 00128c7..0000000 --- a/help.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "os" - -func help() { - writeUsage(os.Stdout) -} diff --git a/internal/arranger/arranger.go b/internal/arranger/arranger.go new file mode 100644 index 0000000..0f124da --- /dev/null +++ b/internal/arranger/arranger.go @@ -0,0 +1,64 @@ +package arranger + +import ( + "go/ast" + "go/token" + + "github.com/jdeflander/goarrange/internal/arranger/internal/index" +) + +// Arranger represents a source code arranger for go packages. +type Arranger struct { + index index.Index + set *token.FileSet +} + +// New creates a new arranger for the given package and file set. +// +// The given package must have been parsed with the given file set. +func New(pkg *ast.Package, set *token.FileSet) Arranger { + idx := index.New(pkg) + return Arranger{ + index: idx, + set: set, + } +} + +// Arrange arranges the given file with the given arranger. +// +// The given file must be part of the given arranger's package, and its contents should be represented by the given +// bytes. This method returns an arranged copy of the given contents. +func (a Arranger) Arrange(file *ast.File, src []byte) []byte { + indexes := a.index.Sort(file.Decls) + size := len(src) + dst := make([]byte, size) + dstOffset := 0 + mp := ast.NewCommentMap(a.set, file, file.Comments) + srcOffset := 0 + + for dstIndex, srcIndex := range indexes { + dstPrefix := dst[dstOffset:] + dstStart, dstEnd := bounds(file.Decls, dstIndex, mp, a.set) + srcPrefix := src[srcOffset:dstStart] + dstOffset += copy(dstPrefix, srcPrefix) + + dstInfix := dst[dstOffset:] + srcStart, srcEnd := bounds(file.Decls, srcIndex, mp, a.set) + srcInfix := src[srcStart:srcEnd] + dstOffset += copy(dstInfix, srcInfix) + + srcOffset = dstEnd + } + + dstSuffix := dst[dstOffset:] + srcSuffix := src[srcOffset:] + copy(dstSuffix, srcSuffix) + return dst +} + +// Arranged checks whether the given file is arranged according to the given arranger. +// +// The given file must be part of the given arranger's package. +func (a Arranger) Arranged(file *ast.File) bool { + return a.index.Sorted(file.Decls) +} diff --git a/internal/arranger/arranger_test.go b/internal/arranger/arranger_test.go new file mode 100644 index 0000000..2deeab4 --- /dev/null +++ b/internal/arranger/arranger_test.go @@ -0,0 +1,68 @@ +package arranger_test + +import ( + "bytes" + "fmt" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/jdeflander/goarrange/internal/arranger" +) + +func TestArranger(t *testing.T) { + if err := filepath.Walk("testdata", walk); err != nil { + t.Error(err) + } +} + +func notGolden(info os.FileInfo) bool { + name := info.Name() + return !strings.HasSuffix(name, ".golden.go") +} + +func walk(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed walking to file at '%s': %w", path, err) + } + if !info.IsDir() { + return nil + } + + set := token.NewFileSet() + packages, err := parser.ParseDir(set, path, notGolden, parser.ParseComments) + if err != nil { + return fmt.Errorf("failed parsing directory at '%s': %v", path, err) + } + + for _, pkg := range packages { + a := arranger.New(pkg, set) + for name, file := range pkg.Files { + bs, err := ioutil.ReadFile(name) + if err != nil { + return fmt.Errorf("failed reading file at '%s': %v", name, err) + } + gotArrange := a.Arrange(file, bs) + golden := fmt.Sprintf("%slden.go", name) + wantArrange, err := ioutil.ReadFile(golden) + if err != nil { + return fmt.Errorf("failed reading golden file at '%s': %v", golden, err) + } + if diff := cmp.Diff(gotArrange, wantArrange); diff != "" { + return fmt.Errorf("arrange output mismatch for file at '%s':\n%s", name, diff) + } + + gotArranged := a.Arranged(file) + wantArranged := bytes.Equal(bs, gotArrange) + if diff := cmp.Diff(gotArranged, wantArranged); diff != "" { + return fmt.Errorf("arranged output mismatch for file at '%s':\n%s", name, diff) + } + } + } + return nil +} diff --git a/internal/arranger/internal/index/index.go b/internal/arranger/internal/index/index.go new file mode 100644 index 0000000..dc20444 --- /dev/null +++ b/internal/arranger/internal/index/index.go @@ -0,0 +1,80 @@ +package index + +import ( + "go/ast" + "go/doc" + "sort" +) + +// Index represents an index of a Go package's arrangeable declarations. +type Index struct { + decls map[ast.Decl]int +} + +// New returns an index for the given Go package. +func New(pkg *ast.Package) Index { + decls := map[ast.Decl]int{} + idx := Index{decls: decls} + p := doc.New(pkg, "", doc.AllDecls|doc.PreserveAST) + + idx.appendValues(p.Consts) + idx.appendValues(p.Vars) + idx.appendFuncs(p.Funcs) + + for _, typ := range p.Types { + idx.append(typ.Decl) + idx.appendValues(typ.Consts) + idx.appendValues(typ.Vars) + idx.appendFuncs(typ.Funcs) + idx.appendFuncs(typ.Methods) + } + return idx +} + +// Sort returns the indices of the given declarations, sorted according to the given index. +func (i Index) Sort(decls []ast.Decl) []int { + records := i.records(decls) + sort.Stable(records) + + var indexes []int + for _, record := range records { + indexes = append(indexes, record.index) + } + return indexes +} + +// Sorted checks whether the given declarations are sorted according to the given index. +func (i Index) Sorted(decls []ast.Decl) bool { + records := i.records(decls) + return sort.IsSorted(records) +} + +func (i Index) append(decl ast.Decl) { + i.decls[decl] = len(i.decls) +} + +func (i Index) appendFuncs(funcs []*doc.Func) { + for _, fun := range funcs { + i.append(fun.Decl) + } +} + +func (i Index) appendValues(values []*doc.Value) { + for _, value := range values { + i.append(value.Decl) + } +} + +func (i Index) records(decls []ast.Decl) records { + var records records + for index, decl := range decls { + key, ok := i.decls[decl] + record := record{ + index: index, + key: key, + ok: ok, + } + records = append(records, record) + } + return records +} diff --git a/internal/arranger/internal/index/index_test.go b/internal/arranger/internal/index/index_test.go new file mode 100644 index 0000000..2922936 --- /dev/null +++ b/internal/arranger/internal/index/index_test.go @@ -0,0 +1,63 @@ +package index_test + +import ( + "encoding/json" + "fmt" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/jdeflander/goarrange/internal/arranger/internal/index" +) + +func TestIndex(t *testing.T) { + if err := filepath.Walk("testdata", walk); err != nil { + t.Error(err) + } +} + +func walk(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed walking to file at '%s': %w", path, err) + } + if !info.IsDir() { + return nil + } + + set := token.NewFileSet() + packages, err := parser.ParseDir(set, path, nil, 0) + if err != nil { + return fmt.Errorf("failed parsing directory at '%s': %v", path, err) + } + + for _, pkg := range packages { + idx := index.New(pkg) + for name, file := range pkg.Files { + gotSort := idx.Sort(file.Decls) + golden := fmt.Sprintf("%slden.json", name) + bytes, err := ioutil.ReadFile(golden) + if err != nil { + return fmt.Errorf("failed reading golden file at '%s': %v", golden, err) + } + var wantSort []int + if err := json.Unmarshal(bytes, &wantSort); err != nil { + return fmt.Errorf("failed unmarshalling golden file at '%s': %v", golden, err) + } + if diff := cmp.Diff(gotSort, wantSort); diff != "" { + return fmt.Errorf("sort mismatch for file at '%s' (-got +want):\n%s", name, diff) + } + + gotSorted := idx.Sorted(file.Decls) + wantSorted := sort.IntsAreSorted(wantSort) + if diff := cmp.Diff(gotSorted, wantSorted); diff != "" { + return fmt.Errorf("sorted mismatch for file at '%s' (-got +want):\n%s", name, diff) + } + } + } + return nil +} diff --git a/internal/index/record.go b/internal/arranger/internal/index/record.go similarity index 100% rename from internal/index/record.go rename to internal/arranger/internal/index/record.go diff --git a/internal/arranger/internal/index/testdata/container/ring/ring.go b/internal/arranger/internal/index/testdata/container/ring/ring.go new file mode 100644 index 0000000..6d3b3e5 --- /dev/null +++ b/internal/arranger/internal/index/testdata/container/ring/ring.go @@ -0,0 +1,141 @@ +// Copyright 2009 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 ring implements operations on circular lists. +package ring + +// A Ring is an element of a circular list, or ring. +// Rings do not have a beginning or end; a pointer to any ring element +// serves as reference to the entire ring. Empty rings are represented +// as nil Ring pointers. The zero value for a Ring is a one-element +// ring with a nil Value. +// +type Ring struct { + next, prev *Ring + Value interface{} // for use by client; untouched by this library +} + +func (r *Ring) init() *Ring { + r.next = r + r.prev = r + return r +} + +// Next returns the next ring element. r must not be empty. +func (r *Ring) Next() *Ring { + if r.next == nil { + return r.init() + } + return r.next +} + +// Prev returns the previous ring element. r must not be empty. +func (r *Ring) Prev() *Ring { + if r.next == nil { + return r.init() + } + return r.prev +} + +// Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0) +// in the ring and returns that ring element. r must not be empty. +// +func (r *Ring) Move(n int) *Ring { + if r.next == nil { + return r.init() + } + switch { + case n < 0: + for ; n < 0; n++ { + r = r.prev + } + case n > 0: + for ; n > 0; n-- { + r = r.next + } + } + return r +} + +// New creates a ring of n elements. +func New(n int) *Ring { + if n <= 0 { + return nil + } + r := new(Ring) + p := r + for i := 1; i < n; i++ { + p.next = &Ring{prev: p} + p = p.next + } + p.next = r + r.prev = p + return r +} + +// Link connects ring r with ring s such that r.Next() +// becomes s and returns the original value for r.Next(). +// r must not be empty. +// +// If r and s point to the same ring, linking +// them removes the elements between r and s from the ring. +// The removed elements form a subring and the result is a +// reference to that subring (if no elements were removed, +// the result is still the original value for r.Next(), +// and not nil). +// +// If r and s point to different rings, linking +// them creates a single ring with the elements of s inserted +// after r. The result points to the element following the +// last element of s after insertion. +// +func (r *Ring) Link(s *Ring) *Ring { + n := r.Next() + if s != nil { + p := s.Prev() + // Note: Cannot use multiple assignment because + // evaluation order of LHS is not specified. + r.next = s + s.prev = r + n.prev = p + p.next = n + } + return n +} + +// Unlink removes n % r.Len() elements from the ring r, starting +// at r.Next(). If n % r.Len() == 0, r remains unchanged. +// The result is the removed subring. r must not be empty. +// +func (r *Ring) Unlink(n int) *Ring { + if n <= 0 { + return nil + } + return r.Link(r.Move(n + 1)) +} + +// Len computes the number of elements in ring r. +// It executes in time proportional to the number of elements. +// +func (r *Ring) Len() int { + n := 0 + if r != nil { + n = 1 + for p := r.Next(); p != r; p = p.next { + n++ + } + } + return n +} + +// Do calls function f on each element of the ring, in forward order. +// The behavior of Do is undefined if f changes *r. +func (r *Ring) Do(f func(interface{})) { + if r != nil { + f(r.Value) + for p := r.Next(); p != r; p = p.next { + f(p.Value) + } + } +} diff --git a/internal/arranger/internal/index/testdata/container/ring/ring.golden.json b/internal/arranger/internal/index/testdata/container/ring/ring.golden.json new file mode 100644 index 0000000..02dec02 --- /dev/null +++ b/internal/arranger/internal/index/testdata/container/ring/ring.golden.json @@ -0,0 +1,12 @@ +[ + 0, + 5, + 9, + 8, + 6, + 4, + 2, + 3, + 7, + 1 +] diff --git a/internal/arranger/internal/index/testdata/encoding/encoding.go b/internal/arranger/internal/index/testdata/encoding/encoding.go new file mode 100644 index 0000000..cc5a536 --- /dev/null +++ b/internal/arranger/internal/index/testdata/encoding/encoding.go @@ -0,0 +1,48 @@ +// Copyright 2013 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 encoding defines interfaces shared by other packages that +// convert data to and from byte-level and textual representations. +// Packages that check for these interfaces include encoding/gob, +// encoding/json, and encoding/xml. As a result, implementing an +// interface once can make a type useful in multiple encodings. +// Standard types that implement these interfaces include time.Time and net.IP. +// The interfaces come in pairs that produce and consume encoded data. +package encoding + +// BinaryMarshaler is the interface implemented by an object that can +// marshal itself into a binary form. +// +// MarshalBinary encodes the receiver into a binary form and returns the result. +type BinaryMarshaler interface { + MarshalBinary() (data []byte, err error) +} + +// BinaryUnmarshaler is the interface implemented by an object that can +// unmarshal a binary representation of itself. +// +// UnmarshalBinary must be able to decode the form generated by MarshalBinary. +// UnmarshalBinary must copy the data if it wishes to retain the data +// after returning. +type BinaryUnmarshaler interface { + UnmarshalBinary(data []byte) error +} + +// TextMarshaler is the interface implemented by an object that can +// marshal itself into a textual form. +// +// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. +type TextMarshaler interface { + MarshalText() (text []byte, err error) +} + +// TextUnmarshaler is the interface implemented by an object that can +// unmarshal a textual representation of itself. +// +// UnmarshalText must be able to decode the form generated by MarshalText. +// UnmarshalText must copy the text if it wishes to retain the text +// after returning. +type TextUnmarshaler interface { + UnmarshalText(text []byte) error +} diff --git a/internal/arranger/internal/index/testdata/encoding/encoding.golden.json b/internal/arranger/internal/index/testdata/encoding/encoding.golden.json new file mode 100644 index 0000000..82195ce --- /dev/null +++ b/internal/arranger/internal/index/testdata/encoding/encoding.golden.json @@ -0,0 +1,6 @@ +[ + 0, + 1, + 2, + 3 +] diff --git a/internal/arranger/internal/index/testdata/io/ioutil/ioutil.go b/internal/arranger/internal/index/testdata/io/ioutil/ioutil.go new file mode 100644 index 0000000..b1cb841 --- /dev/null +++ b/internal/arranger/internal/index/testdata/io/ioutil/ioutil.go @@ -0,0 +1,158 @@ +// Copyright 2009 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 ioutil implements some I/O utility functions. +package ioutil + +import ( + "bytes" + "io" + "os" + "sort" + "sync" +) + +// readAll reads from r until an error or EOF and returns the data it read +// from the internal buffer allocated with a specified capacity. +func readAll(r io.Reader, capacity int64) (b []byte, err error) { + var buf bytes.Buffer + // If the buffer overflows, we will get bytes.ErrTooLarge. + // Return that as an error. Any other panic remains. + defer func() { + e := recover() + if e == nil { + return + } + if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { + err = panicErr + } else { + panic(e) + } + }() + if int64(int(capacity)) == capacity { + buf.Grow(int(capacity)) + } + _, err = buf.ReadFrom(r) + return buf.Bytes(), err +} + +// ReadAll reads from r until an error or EOF and returns the data it read. +// A successful call returns err == nil, not err == EOF. Because ReadAll is +// defined to read from src until EOF, it does not treat an EOF from Read +// as an error to be reported. +func ReadAll(r io.Reader) ([]byte, error) { + return readAll(r, bytes.MinRead) +} + +// ReadFile reads the file named by filename and returns the contents. +// A successful call returns err == nil, not err == EOF. Because ReadFile +// reads the whole file, it does not treat an EOF from Read as an error +// to be reported. +func ReadFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + // It's a good but not certain bet that FileInfo will tell us exactly how much to + // read, so let's try it but be prepared for the answer to be wrong. + var n int64 = bytes.MinRead + + if fi, err := f.Stat(); err == nil { + // As initial capacity for readAll, use Size + a little extra in case Size + // is zero, and to avoid another allocation after Read has filled the + // buffer. The readAll call will read into its allocated internal buffer + // cheaply. If the size was wrong, we'll either waste some space off the end + // or reallocate as needed, but in the overwhelmingly common case we'll get + // it just right. + if size := fi.Size() + bytes.MinRead; size > n { + n = size + } + } + return readAll(f, n) +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm +// (before umask); otherwise WriteFile truncates it before writing. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +// ReadDir reads the directory named by dirname and returns +// a list of directory entries sorted by filename. +func ReadDir(dirname string) ([]os.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) + return list, nil +} + +type nopCloser struct { + io.Reader +} + +func (nopCloser) Close() error { return nil } + +// NopCloser returns a ReadCloser with a no-op Close method wrapping +// the provided Reader r. +func NopCloser(r io.Reader) io.ReadCloser { + return nopCloser{r} +} + +type devNull int + +// devNull implements ReaderFrom as an optimization so io.Copy to +// ioutil.Discard can avoid doing unnecessary work. +var _ io.ReaderFrom = devNull(0) + +func (devNull) Write(p []byte) (int, error) { + return len(p), nil +} + +func (devNull) WriteString(s string) (int, error) { + return len(s), nil +} + +var blackHolePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 8192) + return &b + }, +} + +func (devNull) ReadFrom(r io.Reader) (n int64, err error) { + bufp := blackHolePool.Get().(*[]byte) + readSize := 0 + for { + readSize, err = r.Read(*bufp) + n += int64(readSize) + if err != nil { + blackHolePool.Put(bufp) + if err == io.EOF { + return n, nil + } + return + } + } +} + +// Discard is an io.Writer on which all Write calls succeed +// without doing anything. +var Discard io.Writer = devNull(0) diff --git a/internal/arranger/internal/index/testdata/io/ioutil/ioutil.golden.json b/internal/arranger/internal/index/testdata/io/ioutil/ioutil.golden.json new file mode 100644 index 0000000..d6f2b50 --- /dev/null +++ b/internal/arranger/internal/index/testdata/io/ioutil/ioutil.golden.json @@ -0,0 +1,18 @@ +[ + 0, + 15, + 10, + 13, + 8, + 2, + 5, + 3, + 4, + 1, + 9, + 14, + 11, + 12, + 6, + 7 +] diff --git a/internal/arranger/internal/index/testdata/io/ioutil/tempfile.go b/internal/arranger/internal/index/testdata/io/ioutil/tempfile.go new file mode 100644 index 0000000..3aa23c5 --- /dev/null +++ b/internal/arranger/internal/index/testdata/io/ioutil/tempfile.go @@ -0,0 +1,124 @@ +// Copyright 2010 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 ioutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} + +func nextRandom() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} + +// TempFile creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting *os.File. +// The filename is generated by taking pattern and adding a random +// string to the end. If pattern includes a "*", the random string +// replaces the last "*". +// If dir is the empty string, TempFile uses the default directory +// for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously +// will not choose the same file. The caller can use f.Name() +// to find the pathname of the file. It is the caller's responsibility +// to remove the file when no longer needed. +func TempFile(dir, pattern string) (f *os.File, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+nextRandom()+suffix) + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + break + } + return +} + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string) { + if pos := strings.LastIndex(pattern, "*"); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return +} + +// TempDir creates a new temporary directory in the directory dir. +// The directory name is generated by taking pattern and applying a +// random string to the end. If pattern includes a "*", the random string +// replaces the last "*". TempDir returns the name of the new directory. +// If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func TempDir(dir, pattern string) (name string, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + try := filepath.Join(dir, prefix+nextRandom()+suffix) + err = os.Mkdir(try, 0700) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + if os.IsNotExist(err) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return "", err + } + } + if err == nil { + name = try + } + break + } + return +} diff --git a/internal/arranger/internal/index/testdata/io/ioutil/tempfile.golden.json b/internal/arranger/internal/index/testdata/io/ioutil/tempfile.golden.json new file mode 100644 index 0000000..b187578 --- /dev/null +++ b/internal/arranger/internal/index/testdata/io/ioutil/tempfile.golden.json @@ -0,0 +1,10 @@ +[ + 0, + 1, + 2, + 7, + 5, + 4, + 6, + 3 +] diff --git a/internal/arranger/testdata/container/ring/ring.go b/internal/arranger/testdata/container/ring/ring.go new file mode 100644 index 0000000..6d3b3e5 --- /dev/null +++ b/internal/arranger/testdata/container/ring/ring.go @@ -0,0 +1,141 @@ +// Copyright 2009 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 ring implements operations on circular lists. +package ring + +// A Ring is an element of a circular list, or ring. +// Rings do not have a beginning or end; a pointer to any ring element +// serves as reference to the entire ring. Empty rings are represented +// as nil Ring pointers. The zero value for a Ring is a one-element +// ring with a nil Value. +// +type Ring struct { + next, prev *Ring + Value interface{} // for use by client; untouched by this library +} + +func (r *Ring) init() *Ring { + r.next = r + r.prev = r + return r +} + +// Next returns the next ring element. r must not be empty. +func (r *Ring) Next() *Ring { + if r.next == nil { + return r.init() + } + return r.next +} + +// Prev returns the previous ring element. r must not be empty. +func (r *Ring) Prev() *Ring { + if r.next == nil { + return r.init() + } + return r.prev +} + +// Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0) +// in the ring and returns that ring element. r must not be empty. +// +func (r *Ring) Move(n int) *Ring { + if r.next == nil { + return r.init() + } + switch { + case n < 0: + for ; n < 0; n++ { + r = r.prev + } + case n > 0: + for ; n > 0; n-- { + r = r.next + } + } + return r +} + +// New creates a ring of n elements. +func New(n int) *Ring { + if n <= 0 { + return nil + } + r := new(Ring) + p := r + for i := 1; i < n; i++ { + p.next = &Ring{prev: p} + p = p.next + } + p.next = r + r.prev = p + return r +} + +// Link connects ring r with ring s such that r.Next() +// becomes s and returns the original value for r.Next(). +// r must not be empty. +// +// If r and s point to the same ring, linking +// them removes the elements between r and s from the ring. +// The removed elements form a subring and the result is a +// reference to that subring (if no elements were removed, +// the result is still the original value for r.Next(), +// and not nil). +// +// If r and s point to different rings, linking +// them creates a single ring with the elements of s inserted +// after r. The result points to the element following the +// last element of s after insertion. +// +func (r *Ring) Link(s *Ring) *Ring { + n := r.Next() + if s != nil { + p := s.Prev() + // Note: Cannot use multiple assignment because + // evaluation order of LHS is not specified. + r.next = s + s.prev = r + n.prev = p + p.next = n + } + return n +} + +// Unlink removes n % r.Len() elements from the ring r, starting +// at r.Next(). If n % r.Len() == 0, r remains unchanged. +// The result is the removed subring. r must not be empty. +// +func (r *Ring) Unlink(n int) *Ring { + if n <= 0 { + return nil + } + return r.Link(r.Move(n + 1)) +} + +// Len computes the number of elements in ring r. +// It executes in time proportional to the number of elements. +// +func (r *Ring) Len() int { + n := 0 + if r != nil { + n = 1 + for p := r.Next(); p != r; p = p.next { + n++ + } + } + return n +} + +// Do calls function f on each element of the ring, in forward order. +// The behavior of Do is undefined if f changes *r. +func (r *Ring) Do(f func(interface{})) { + if r != nil { + f(r.Value) + for p := r.Next(); p != r; p = p.next { + f(p.Value) + } + } +} diff --git a/internal/arranger/testdata/container/ring/ring.golden.go b/internal/arranger/testdata/container/ring/ring.golden.go new file mode 100644 index 0000000..0f82bea --- /dev/null +++ b/internal/arranger/testdata/container/ring/ring.golden.go @@ -0,0 +1,141 @@ +// Copyright 2009 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 ring implements operations on circular lists. +package ring + +// A Ring is an element of a circular list, or ring. +// Rings do not have a beginning or end; a pointer to any ring element +// serves as reference to the entire ring. Empty rings are represented +// as nil Ring pointers. The zero value for a Ring is a one-element +// ring with a nil Value. +// +type Ring struct { + next, prev *Ring + Value interface{} // for use by client; untouched by this library +} + +// New creates a ring of n elements. +func New(n int) *Ring { + if n <= 0 { + return nil + } + r := new(Ring) + p := r + for i := 1; i < n; i++ { + p.next = &Ring{prev: p} + p = p.next + } + p.next = r + r.prev = p + return r +} + +// Do calls function f on each element of the ring, in forward order. +// The behavior of Do is undefined if f changes *r. +func (r *Ring) Do(f func(interface{})) { + if r != nil { + f(r.Value) + for p := r.Next(); p != r; p = p.next { + f(p.Value) + } + } +} + +// Len computes the number of elements in ring r. +// It executes in time proportional to the number of elements. +// +func (r *Ring) Len() int { + n := 0 + if r != nil { + n = 1 + for p := r.Next(); p != r; p = p.next { + n++ + } + } + return n +} + +// Link connects ring r with ring s such that r.Next() +// becomes s and returns the original value for r.Next(). +// r must not be empty. +// +// If r and s point to the same ring, linking +// them removes the elements between r and s from the ring. +// The removed elements form a subring and the result is a +// reference to that subring (if no elements were removed, +// the result is still the original value for r.Next(), +// and not nil). +// +// If r and s point to different rings, linking +// them creates a single ring with the elements of s inserted +// after r. The result points to the element following the +// last element of s after insertion. +// +func (r *Ring) Link(s *Ring) *Ring { + n := r.Next() + if s != nil { + p := s.Prev() + // Note: Cannot use multiple assignment because + // evaluation order of LHS is not specified. + r.next = s + s.prev = r + n.prev = p + p.next = n + } + return n +} + +// Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0) +// in the ring and returns that ring element. r must not be empty. +// +func (r *Ring) Move(n int) *Ring { + if r.next == nil { + return r.init() + } + switch { + case n < 0: + for ; n < 0; n++ { + r = r.prev + } + case n > 0: + for ; n > 0; n-- { + r = r.next + } + } + return r +} + +// Next returns the next ring element. r must not be empty. +func (r *Ring) Next() *Ring { + if r.next == nil { + return r.init() + } + return r.next +} + +// Prev returns the previous ring element. r must not be empty. +func (r *Ring) Prev() *Ring { + if r.next == nil { + return r.init() + } + return r.prev +} + +// Unlink removes n % r.Len() elements from the ring r, starting +// at r.Next(). If n % r.Len() == 0, r remains unchanged. +// The result is the removed subring. r must not be empty. +// +func (r *Ring) Unlink(n int) *Ring { + if n <= 0 { + return nil + } + return r.Link(r.Move(n + 1)) +} + +func (r *Ring) init() *Ring { + r.next = r + r.prev = r + return r +} diff --git a/internal/arranger/testdata/encoding/encoding.go b/internal/arranger/testdata/encoding/encoding.go new file mode 100644 index 0000000..cc5a536 --- /dev/null +++ b/internal/arranger/testdata/encoding/encoding.go @@ -0,0 +1,48 @@ +// Copyright 2013 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 encoding defines interfaces shared by other packages that +// convert data to and from byte-level and textual representations. +// Packages that check for these interfaces include encoding/gob, +// encoding/json, and encoding/xml. As a result, implementing an +// interface once can make a type useful in multiple encodings. +// Standard types that implement these interfaces include time.Time and net.IP. +// The interfaces come in pairs that produce and consume encoded data. +package encoding + +// BinaryMarshaler is the interface implemented by an object that can +// marshal itself into a binary form. +// +// MarshalBinary encodes the receiver into a binary form and returns the result. +type BinaryMarshaler interface { + MarshalBinary() (data []byte, err error) +} + +// BinaryUnmarshaler is the interface implemented by an object that can +// unmarshal a binary representation of itself. +// +// UnmarshalBinary must be able to decode the form generated by MarshalBinary. +// UnmarshalBinary must copy the data if it wishes to retain the data +// after returning. +type BinaryUnmarshaler interface { + UnmarshalBinary(data []byte) error +} + +// TextMarshaler is the interface implemented by an object that can +// marshal itself into a textual form. +// +// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. +type TextMarshaler interface { + MarshalText() (text []byte, err error) +} + +// TextUnmarshaler is the interface implemented by an object that can +// unmarshal a textual representation of itself. +// +// UnmarshalText must be able to decode the form generated by MarshalText. +// UnmarshalText must copy the text if it wishes to retain the text +// after returning. +type TextUnmarshaler interface { + UnmarshalText(text []byte) error +} diff --git a/internal/arranger/testdata/encoding/encoding.golden.go b/internal/arranger/testdata/encoding/encoding.golden.go new file mode 100644 index 0000000..cc5a536 --- /dev/null +++ b/internal/arranger/testdata/encoding/encoding.golden.go @@ -0,0 +1,48 @@ +// Copyright 2013 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 encoding defines interfaces shared by other packages that +// convert data to and from byte-level and textual representations. +// Packages that check for these interfaces include encoding/gob, +// encoding/json, and encoding/xml. As a result, implementing an +// interface once can make a type useful in multiple encodings. +// Standard types that implement these interfaces include time.Time and net.IP. +// The interfaces come in pairs that produce and consume encoded data. +package encoding + +// BinaryMarshaler is the interface implemented by an object that can +// marshal itself into a binary form. +// +// MarshalBinary encodes the receiver into a binary form and returns the result. +type BinaryMarshaler interface { + MarshalBinary() (data []byte, err error) +} + +// BinaryUnmarshaler is the interface implemented by an object that can +// unmarshal a binary representation of itself. +// +// UnmarshalBinary must be able to decode the form generated by MarshalBinary. +// UnmarshalBinary must copy the data if it wishes to retain the data +// after returning. +type BinaryUnmarshaler interface { + UnmarshalBinary(data []byte) error +} + +// TextMarshaler is the interface implemented by an object that can +// marshal itself into a textual form. +// +// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. +type TextMarshaler interface { + MarshalText() (text []byte, err error) +} + +// TextUnmarshaler is the interface implemented by an object that can +// unmarshal a textual representation of itself. +// +// UnmarshalText must be able to decode the form generated by MarshalText. +// UnmarshalText must copy the text if it wishes to retain the text +// after returning. +type TextUnmarshaler interface { + UnmarshalText(text []byte) error +} diff --git a/internal/arranger/testdata/io/ioutil/ioutil.go b/internal/arranger/testdata/io/ioutil/ioutil.go new file mode 100644 index 0000000..b1cb841 --- /dev/null +++ b/internal/arranger/testdata/io/ioutil/ioutil.go @@ -0,0 +1,158 @@ +// Copyright 2009 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 ioutil implements some I/O utility functions. +package ioutil + +import ( + "bytes" + "io" + "os" + "sort" + "sync" +) + +// readAll reads from r until an error or EOF and returns the data it read +// from the internal buffer allocated with a specified capacity. +func readAll(r io.Reader, capacity int64) (b []byte, err error) { + var buf bytes.Buffer + // If the buffer overflows, we will get bytes.ErrTooLarge. + // Return that as an error. Any other panic remains. + defer func() { + e := recover() + if e == nil { + return + } + if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { + err = panicErr + } else { + panic(e) + } + }() + if int64(int(capacity)) == capacity { + buf.Grow(int(capacity)) + } + _, err = buf.ReadFrom(r) + return buf.Bytes(), err +} + +// ReadAll reads from r until an error or EOF and returns the data it read. +// A successful call returns err == nil, not err == EOF. Because ReadAll is +// defined to read from src until EOF, it does not treat an EOF from Read +// as an error to be reported. +func ReadAll(r io.Reader) ([]byte, error) { + return readAll(r, bytes.MinRead) +} + +// ReadFile reads the file named by filename and returns the contents. +// A successful call returns err == nil, not err == EOF. Because ReadFile +// reads the whole file, it does not treat an EOF from Read as an error +// to be reported. +func ReadFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + // It's a good but not certain bet that FileInfo will tell us exactly how much to + // read, so let's try it but be prepared for the answer to be wrong. + var n int64 = bytes.MinRead + + if fi, err := f.Stat(); err == nil { + // As initial capacity for readAll, use Size + a little extra in case Size + // is zero, and to avoid another allocation after Read has filled the + // buffer. The readAll call will read into its allocated internal buffer + // cheaply. If the size was wrong, we'll either waste some space off the end + // or reallocate as needed, but in the overwhelmingly common case we'll get + // it just right. + if size := fi.Size() + bytes.MinRead; size > n { + n = size + } + } + return readAll(f, n) +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm +// (before umask); otherwise WriteFile truncates it before writing. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +// ReadDir reads the directory named by dirname and returns +// a list of directory entries sorted by filename. +func ReadDir(dirname string) ([]os.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) + return list, nil +} + +type nopCloser struct { + io.Reader +} + +func (nopCloser) Close() error { return nil } + +// NopCloser returns a ReadCloser with a no-op Close method wrapping +// the provided Reader r. +func NopCloser(r io.Reader) io.ReadCloser { + return nopCloser{r} +} + +type devNull int + +// devNull implements ReaderFrom as an optimization so io.Copy to +// ioutil.Discard can avoid doing unnecessary work. +var _ io.ReaderFrom = devNull(0) + +func (devNull) Write(p []byte) (int, error) { + return len(p), nil +} + +func (devNull) WriteString(s string) (int, error) { + return len(s), nil +} + +var blackHolePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 8192) + return &b + }, +} + +func (devNull) ReadFrom(r io.Reader) (n int64, err error) { + bufp := blackHolePool.Get().(*[]byte) + readSize := 0 + for { + readSize, err = r.Read(*bufp) + n += int64(readSize) + if err != nil { + blackHolePool.Put(bufp) + if err == io.EOF { + return n, nil + } + return + } + } +} + +// Discard is an io.Writer on which all Write calls succeed +// without doing anything. +var Discard io.Writer = devNull(0) diff --git a/internal/arranger/testdata/io/ioutil/ioutil.golden.go b/internal/arranger/testdata/io/ioutil/ioutil.golden.go new file mode 100644 index 0000000..64b37bb --- /dev/null +++ b/internal/arranger/testdata/io/ioutil/ioutil.golden.go @@ -0,0 +1,158 @@ +// Copyright 2009 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 ioutil implements some I/O utility functions. +package ioutil + +import ( + "bytes" + "io" + "os" + "sort" + "sync" +) + +// Discard is an io.Writer on which all Write calls succeed +// without doing anything. +var Discard io.Writer = devNull(0) + +// devNull implements ReaderFrom as an optimization so io.Copy to +// ioutil.Discard can avoid doing unnecessary work. +var _ io.ReaderFrom = devNull(0) + +var blackHolePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 8192) + return &b + }, +} + +// NopCloser returns a ReadCloser with a no-op Close method wrapping +// the provided Reader r. +func NopCloser(r io.Reader) io.ReadCloser { + return nopCloser{r} +} + +// ReadAll reads from r until an error or EOF and returns the data it read. +// A successful call returns err == nil, not err == EOF. Because ReadAll is +// defined to read from src until EOF, it does not treat an EOF from Read +// as an error to be reported. +func ReadAll(r io.Reader) ([]byte, error) { + return readAll(r, bytes.MinRead) +} + +// ReadDir reads the directory named by dirname and returns +// a list of directory entries sorted by filename. +func ReadDir(dirname string) ([]os.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) + return list, nil +} + +// ReadFile reads the file named by filename and returns the contents. +// A successful call returns err == nil, not err == EOF. Because ReadFile +// reads the whole file, it does not treat an EOF from Read as an error +// to be reported. +func ReadFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + // It's a good but not certain bet that FileInfo will tell us exactly how much to + // read, so let's try it but be prepared for the answer to be wrong. + var n int64 = bytes.MinRead + + if fi, err := f.Stat(); err == nil { + // As initial capacity for readAll, use Size + a little extra in case Size + // is zero, and to avoid another allocation after Read has filled the + // buffer. The readAll call will read into its allocated internal buffer + // cheaply. If the size was wrong, we'll either waste some space off the end + // or reallocate as needed, but in the overwhelmingly common case we'll get + // it just right. + if size := fi.Size() + bytes.MinRead; size > n { + n = size + } + } + return readAll(f, n) +} + +// WriteFile writes data to a file named by filename. +// If the file does not exist, WriteFile creates it with permissions perm +// (before umask); otherwise WriteFile truncates it before writing. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err == nil { + err = err1 + } + return err +} + +// readAll reads from r until an error or EOF and returns the data it read +// from the internal buffer allocated with a specified capacity. +func readAll(r io.Reader, capacity int64) (b []byte, err error) { + var buf bytes.Buffer + // If the buffer overflows, we will get bytes.ErrTooLarge. + // Return that as an error. Any other panic remains. + defer func() { + e := recover() + if e == nil { + return + } + if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { + err = panicErr + } else { + panic(e) + } + }() + if int64(int(capacity)) == capacity { + buf.Grow(int(capacity)) + } + _, err = buf.ReadFrom(r) + return buf.Bytes(), err +} + +type devNull int + +func (devNull) ReadFrom(r io.Reader) (n int64, err error) { + bufp := blackHolePool.Get().(*[]byte) + readSize := 0 + for { + readSize, err = r.Read(*bufp) + n += int64(readSize) + if err != nil { + blackHolePool.Put(bufp) + if err == io.EOF { + return n, nil + } + return + } + } +} + +func (devNull) Write(p []byte) (int, error) { + return len(p), nil +} + +func (devNull) WriteString(s string) (int, error) { + return len(s), nil +} + +type nopCloser struct { + io.Reader +} + +func (nopCloser) Close() error { return nil } diff --git a/internal/arranger/testdata/io/ioutil/tempfile.go b/internal/arranger/testdata/io/ioutil/tempfile.go new file mode 100644 index 0000000..3aa23c5 --- /dev/null +++ b/internal/arranger/testdata/io/ioutil/tempfile.go @@ -0,0 +1,124 @@ +// Copyright 2010 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 ioutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} + +func nextRandom() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} + +// TempFile creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting *os.File. +// The filename is generated by taking pattern and adding a random +// string to the end. If pattern includes a "*", the random string +// replaces the last "*". +// If dir is the empty string, TempFile uses the default directory +// for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously +// will not choose the same file. The caller can use f.Name() +// to find the pathname of the file. It is the caller's responsibility +// to remove the file when no longer needed. +func TempFile(dir, pattern string) (f *os.File, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+nextRandom()+suffix) + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + break + } + return +} + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string) { + if pos := strings.LastIndex(pattern, "*"); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return +} + +// TempDir creates a new temporary directory in the directory dir. +// The directory name is generated by taking pattern and applying a +// random string to the end. If pattern includes a "*", the random string +// replaces the last "*". TempDir returns the name of the new directory. +// If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func TempDir(dir, pattern string) (name string, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + try := filepath.Join(dir, prefix+nextRandom()+suffix) + err = os.Mkdir(try, 0700) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + if os.IsNotExist(err) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return "", err + } + } + if err == nil { + name = try + } + break + } + return +} diff --git a/internal/arranger/testdata/io/ioutil/tempfile.golden.go b/internal/arranger/testdata/io/ioutil/tempfile.golden.go new file mode 100644 index 0000000..40aec7f --- /dev/null +++ b/internal/arranger/testdata/io/ioutil/tempfile.golden.go @@ -0,0 +1,124 @@ +// Copyright 2010 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 ioutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +// Random number state. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +var rand uint32 +var randmu sync.Mutex + +// TempDir creates a new temporary directory in the directory dir. +// The directory name is generated by taking pattern and applying a +// random string to the end. If pattern includes a "*", the random string +// replaces the last "*". TempDir returns the name of the new directory. +// If dir is the empty string, TempDir uses the +// default directory for temporary files (see os.TempDir). +// Multiple programs calling TempDir simultaneously +// will not choose the same directory. It is the caller's responsibility +// to remove the directory when no longer needed. +func TempDir(dir, pattern string) (name string, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + try := filepath.Join(dir, prefix+nextRandom()+suffix) + err = os.Mkdir(try, 0700) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + if os.IsNotExist(err) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return "", err + } + } + if err == nil { + name = try + } + break + } + return +} + +// TempFile creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting *os.File. +// The filename is generated by taking pattern and adding a random +// string to the end. If pattern includes a "*", the random string +// replaces the last "*". +// If dir is the empty string, TempFile uses the default directory +// for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously +// will not choose the same file. The caller can use f.Name() +// to find the pathname of the file. It is the caller's responsibility +// to remove the file when no longer needed. +func TempFile(dir, pattern string) (f *os.File, err error) { + if dir == "" { + dir = os.TempDir() + } + + prefix, suffix := prefixAndSuffix(pattern) + + nconflict := 0 + for i := 0; i < 10000; i++ { + name := filepath.Join(dir, prefix+nextRandom()+suffix) + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + if nconflict++; nconflict > 10 { + randmu.Lock() + rand = reseed() + randmu.Unlock() + } + continue + } + break + } + return +} + +func nextRandom() string { + randmu.Lock() + r := rand + if r == 0 { + r = reseed() + } + r = r*1664525 + 1013904223 // constants from Numerical Recipes + rand = r + randmu.Unlock() + return strconv.Itoa(int(1e9 + r%1e9))[1:] +} + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string) { + if pos := strings.LastIndex(pattern, "*"); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return +} + +func reseed() uint32 { + return uint32(time.Now().UnixNano() + int64(os.Getpid())) +} diff --git a/internal/arranger/util.go b/internal/arranger/util.go new file mode 100644 index 0000000..e52b335 --- /dev/null +++ b/internal/arranger/util.go @@ -0,0 +1,42 @@ +package arranger + +import ( + "go/ast" + "go/token" +) + +func bounds(decls []ast.Decl, index int, mp ast.CommentMap, set *token.FileSet) (int, int) { + decl := decls[index] + minStart := minStart(decls, index, mp) + start := decl.Pos() + for _, group := range mp.Filter(decl).Comments() { + if group.Pos() > minStart && group.Pos() < start { + start = group.Pos() + } + } + end := end(decl, mp) + return offset(start, set), offset(end, set) +} + +func end(decl ast.Decl, mp ast.CommentMap) token.Pos { + end := decl.End() + for _, group := range mp.Filter(decl).Comments() { + if group.End() > end { + end = group.End() + } + } + return end +} + +func minStart(decls []ast.Decl, index int, mp ast.CommentMap) token.Pos { + if index == 0 { + return token.NoPos + } + decl := decls[index-1] + return end(decl, mp) +} + +func offset(pos token.Pos, set *token.FileSet) int { + position := set.Position(pos) + return position.Offset +} diff --git a/internal/index/index.go b/internal/index/index.go deleted file mode 100644 index 5d559ae..0000000 --- a/internal/index/index.go +++ /dev/null @@ -1,49 +0,0 @@ -package index - -import ( - "go/ast" - "sort" -) - -type Index struct { - decls map[ast.Decl]int -} - -func New() Index { - decls := map[ast.Decl]int{} - return Index{decls: decls} -} - -func (i Index) Append(decl ast.Decl) { - i.decls[decl] = len(i.decls) -} - -func (i Index) IsSorted(decls []ast.Decl) bool { - records := i.records(decls) - return sort.IsSorted(records) -} - -func (i Index) Sort(decls []ast.Decl) []int { - records := i.records(decls) - sort.Stable(records) - - var indexes []int - for _, record := range records { - indexes = append(indexes, record.index) - } - return indexes -} - -func (i Index) records(decls []ast.Decl) records { - var records records - for index, decl := range decls { - key, ok := i.decls[decl] - record := record{ - index: index, - key: key, - ok: ok, - } - records = append(records, record) - } - return records -} diff --git a/main.go b/main.go index 5a52f4d..530ed37 100644 --- a/main.go +++ b/main.go @@ -2,37 +2,84 @@ package main import ( "flag" + "fmt" "io/ioutil" + "log" "os" + "path/filepath" ) +const usage = `Automatic arrangement of Go source code + +Usage: + %[1]s help + %[1]s run [-d] [-p=] [-r] + +Options: + -d Dry-run listing unarranged files + -p= Path of file or directory to arrange [default: .] + -r Walk directories recursively +` + func main() { + log.SetFlags(0) + path := os.Args[0] + name := filepath.Base(path) + prefix := fmt.Sprintf("%s: ", name) + log.SetPrefix(prefix) + if len(os.Args) < 2 { - failWithUsage() + log.Fatalf("missing command, run '%s help' for usage information", name) } switch os.Args[1] { case "help": - help() + if _, err := fmt.Printf(usage, name); err != nil { + log.Fatalf("failed printing usage information: %v", err) + } case "run": set := flag.NewFlagSet("", flag.ContinueOnError) - dryRun := set.Bool("d", false, "") - path := set.String("p", ".", "") - recursive := set.Bool("r", false, "") + var dryRun bool + set.BoolVar(&dryRun, "d", false, "") + var path string + set.StringVar(&path, "p", ".", "") + var recursive bool + set.BoolVar(&recursive, "r", false, "") set.SetOutput(ioutil.Discard) args := os.Args[2:] if err := set.Parse(args); err != nil { - failWithUsage() + log.Fatalf("invalid options, run '%s help' for usage information", name) + } + + directory, filename, err := split(path) + if err != nil { + log.Fatalf("failed splitting path: %v", err) } - if err := run(*path, *recursive, *dryRun); err != nil { - fprintf(os.Stderr, "failed running: %s\n", err) - os.Exit(1) + if filename == "" && recursive { + walk := func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed walking to file at '%s': %w", path, err) + } + if !info.IsDir() { + return nil + } + + if err := arrange(path, "", dryRun); err != nil { + return fmt.Errorf("failed arranging directory at '%s': %w", path, err) + } + return nil + } + if err := filepath.Walk(directory, walk); err != nil { + log.Fatalf("failed walking directory at '%s': %v", directory, err) + } + } else if err := arrange(directory, filename, dryRun); err != nil { + log.Fatalf("failed arranging directory at '%s': %v", directory, err) } default: - failWithUsage() + log.Fatalf("invalid command, run '%s help' for usage information", name) } } diff --git a/run.go b/run.go deleted file mode 100644 index 4ab3a5e..0000000 --- a/run.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "github.com/jdeflander/goarrange/internal/index" - "go/ast" - "go/doc" - "go/parser" - "go/token" - "io/ioutil" - "os" - "path/filepath" -) - -func appendFuncs(idx index.Index, funcs []*doc.Func) { - for _, fun := range funcs { - idx.Append(fun.Decl) - } -} - -func appendValues(idx index.Index, values []*doc.Value) { - for _, value := range values { - idx.Append(value.Decl) - } -} - -func arrangeDirectory(dir, filename string, dryRun bool) error { - set := token.NewFileSet() - packages, err := parser.ParseDir(set, dir, nil, parser.ParseComments) - if err != nil { - return fmt.Errorf("failed parsing: %w", err) - } - - for _, pkg := range packages { - if err := arrangePackage(pkg, set, filename, dryRun); err != nil { - return fmt.Errorf("failed arranging package %q: %w", pkg.Name, err) - } - } - return nil -} - -func arrangeFile(file *ast.File, idx index.Index, path string, set *token.FileSet) error { - indexes := idx.Sort(file.Decls) - mp := ast.NewCommentMap(set, file, file.Comments) - src, err := ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("failed reading: %w", err) - } - var buffer bytes.Buffer - i := 0 - - for dstIndex, srcIndex := range indexes { - dstStart, dstEnd := bounds(file.Decls, dstIndex, mp, set) - prefix := src[i:dstStart] - buffer.Write(prefix) - - srcStart, srcEnd := bounds(file.Decls, srcIndex, mp, set) - infix := src[srcStart:srcEnd] - buffer.Write(infix) - - i = dstEnd - } - - suffix := src[i:] - buffer.Write(suffix) - - dst := buffer.Bytes() - if err := ioutil.WriteFile(path, dst, 0644); err != nil { - return fmt.Errorf("failed writing: %w", err) - } - return nil -} - -func arrangePackage(pkg *ast.Package, set *token.FileSet, filename string, dryRun bool) error { - docs := doc.New(pkg, "", doc.AllDecls|doc.PreserveAST) - idx := index.New() - - appendValues(idx, docs.Consts) - appendValues(idx, docs.Vars) - appendFuncs(idx, docs.Funcs) - - for _, typ := range docs.Types { - idx.Append(typ.Decl) - appendValues(idx, typ.Consts) - appendValues(idx, typ.Vars) - appendFuncs(idx, typ.Funcs) - appendFuncs(idx, typ.Methods) - } - - for path, file := range pkg.Files { - if filename == "" || filepath.Base(path) == filename { - if dryRun { - if !idx.IsSorted(file.Decls) { - fmt.Println(path) - } - } else if err := arrangeFile(file, idx, path, set); err != nil { - return fmt.Errorf("failed arranging file %q: %w", path, err) - } - } - } - return nil -} - -func bounds(decls []ast.Decl, index int, mp ast.CommentMap, set *token.FileSet) (int, int) { - decl := decls[index] - minStart := minStart(decls, index, mp) - start := decl.Pos() - for _, group := range mp.Filter(decl).Comments() { - if group.Pos() > minStart && group.Pos() < start { - start = group.Pos() - } - } - - end := decl.End() - for _, group := range mp.Filter(decl).Comments() { - if group.End() > end { - end = group.End() - } - } - - return offset(start, set), offset(end, set) -} - -func minStart(decls []ast.Decl, index int, mp ast.CommentMap) token.Pos { - if index == 0 { - return token.NoPos - } else { - decl := decls[index-1] - return end(decl, mp) - } -} - -func end(decl ast.Decl, mp ast.CommentMap) token.Pos { - end := decl.End() - for _, group := range mp.Filter(decl).Comments() { - if group.End() > end { - end = group.End() - } - } - return end -} - -func offset(pos token.Pos, set *token.FileSet) int { - position := set.Position(pos) - return position.Offset -} - -func run(path string, recursive bool, dryRun bool) error { - dir, filename, err := split(path) - if err != nil { - return fmt.Errorf("failed splitting path: %w", err) - } - - if filename == "" && recursive { - walk := func(path string, info os.FileInfo, err error) error { - if err != nil { - return fmt.Errorf("failed walking: %w", err) - } - if info.IsDir() { - if err := arrangeDirectory(path, "", dryRun); err != nil { - return fmt.Errorf("failed arranging directory: %w", err) - } - } - return nil - } - if err := filepath.Walk(dir, walk); err != nil { - return fmt.Errorf("failed walking: %w", err) - } - } else { - if err := arrangeDirectory(dir, filename, dryRun); err != nil { - return fmt.Errorf("failed arranging directory: %w", err) - } - } - return nil -} - -func split(path string) (string, string, error) { - info, err := os.Stat(path) - if err != nil { - return "", "", fmt.Errorf("failed checking status of file: %w", err) - } - - if info.IsDir() { - return path, "", nil - } else { - dir := filepath.Dir(path) - filename := filepath.Base(path) - return dir, filename, nil - } -} diff --git a/util.go b/util.go index 9b3e47a..64f2066 100644 --- a/util.go +++ b/util.go @@ -2,33 +2,57 @@ package main import ( "fmt" - "io" + "go/parser" + "go/token" + "io/ioutil" "os" -) + "path/filepath" -func failWithUsage() { - writeUsage(os.Stderr) - os.Exit(1) -} + "github.com/jdeflander/goarrange/internal/arranger" +) -func fprintf(writer io.Writer, format string, args ...interface{}) { - if _, err := fmt.Fprintf(writer, format, args...); err != nil { - panic(err) +func arrange(directory, filename string, dryRun bool) error { + set := token.NewFileSet() + packages, err := parser.ParseDir(set, directory, nil, parser.ParseComments) + if err != nil { + return fmt.Errorf("failed parsing directory at '%s': %v", directory, err) } -} -func writeUsage(writer io.Writer) { - usage := `Automatic arrangement of Go source code + for _, pkg := range packages { + a := arranger.New(pkg, set) + for path, file := range pkg.Files { + if filename != "" && filepath.Base(path) != filename || a.Arranged(file) { + continue + } -Usage: - %[1]s help - %[1]s run [-d] [-p=] [-r] + if dryRun { + if _, err := fmt.Println(path); err != nil { + return fmt.Errorf("failed printing path '%s': %w", path, err) + } + } else { + src, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("failed reading file at '%s': %v", path, err) + } + dst := a.Arrange(file, src) + if err := ioutil.WriteFile(path, dst, 0644); err != nil { + return fmt.Errorf("failed writing file at '%s': %v", path, err) + } + } + } + } + return nil +} -Options: - -d Dry-run listing unarranged files - -p= Path of file or directory to arrange [default: .] - -r Walk directories recursively -` - name := os.Args[0] - fprintf(writer, usage, name) +func split(path string) (string, string, error) { + info, err := os.Stat(path) + if err != nil { + return "", "", fmt.Errorf("failed checking status of file at '%s': %w", path, err) + } + if info.IsDir() { + return path, "", nil + } + dir := filepath.Dir(path) + filename := filepath.Base(path) + return dir, filename, nil }