Skip to content

Commit

Permalink
Merge pull request #16 from jdeflander/feature/15-initial-release
Browse files Browse the repository at this point in the history
Setup initial release
  • Loading branch information
jdeflander authored May 2, 2020
2 parents e48208c + ac99dad commit e9f7fee
Show file tree
Hide file tree
Showing 32 changed files with 1,928 additions and 283 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
issues:
exclude-use-default: false

linters:
enable:
- goimports
- golint
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/jdeflander/goarrange

go 1.13

require github.com/google/go-cmp v0.4.0
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
7 changes: 0 additions & 7 deletions help.go

This file was deleted.

64 changes: 64 additions & 0 deletions internal/arranger/arranger.go
Original file line number Diff line number Diff line change
@@ -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)
}
68 changes: 68 additions & 0 deletions internal/arranger/arranger_test.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions internal/arranger/internal/index/index.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions internal/arranger/internal/index/index_test.go
Original file line number Diff line number Diff line change
@@ -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
}
File renamed without changes.
Loading

0 comments on commit e9f7fee

Please sign in to comment.