Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ version: '3'
vars:
BINARY_NAME: wormatter
CMD_PATH: ./cmd/wormatter
VERSION_PKG: github.com/werf/wormatter/internal/cli
VERSION:
sh: git describe --tags --always --dirty 2>/dev/null || echo "dev"
LDFLAGS: -X {{.VERSION_PKG}}.version={{.VERSION}}

tasks:
default:
desc: Build and test
deps: [build, test]

build:
desc: Build the wormatter binary
cmds:
- go build -o bin/{{.BINARY_NAME}} {{.CMD_PATH}}
- go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}} {{.CMD_PATH}}
sources:
- '**/*.go'
generates:
Expand All @@ -17,11 +25,11 @@ tasks:
build:all:
desc: Build binaries for all platforms
cmds:
- GOOS=linux GOARCH=amd64 go build -o bin/{{.BINARY_NAME}}-linux-amd64 {{.CMD_PATH}}
- GOOS=linux GOARCH=arm64 go build -o bin/{{.BINARY_NAME}}-linux-arm64 {{.CMD_PATH}}
- GOOS=darwin GOARCH=amd64 go build -o bin/{{.BINARY_NAME}}-darwin-amd64 {{.CMD_PATH}}
- GOOS=darwin GOARCH=arm64 go build -o bin/{{.BINARY_NAME}}-darwin-arm64 {{.CMD_PATH}}
- GOOS=windows GOARCH=amd64 go build -o bin/{{.BINARY_NAME}}-windows-amd64.exe {{.CMD_PATH}}
- GOOS=linux GOARCH=amd64 go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}}-linux-amd64 {{.CMD_PATH}}
- GOOS=linux GOARCH=arm64 go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}}-linux-arm64 {{.CMD_PATH}}
- GOOS=darwin GOARCH=amd64 go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}}-darwin-amd64 {{.CMD_PATH}}
- GOOS=darwin GOARCH=arm64 go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}}-darwin-arm64 {{.CMD_PATH}}
- GOOS=windows GOARCH=amd64 go build -ldflags "{{.LDFLAGS}}" -o bin/{{.BINARY_NAME}}-windows-amd64.exe {{.CMD_PATH}}

test:
desc: Run tests
Expand All @@ -36,4 +44,4 @@ tasks:
install:
desc: Install the binary to $GOPATH/bin
cmds:
- go install {{.CMD_PATH}}
- go install -ldflags "{{.LDFLAGS}}" {{.CMD_PATH}}
30 changes: 2 additions & 28 deletions cmd/wormatter/main.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
package main

import (
"fmt"
"os"

"github.com/werf/wormatter/pkg/formatter"
)
import "github.com/werf/wormatter/internal/cli"

func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wormatter <file.go|directory>")
os.Exit(1)
}

path := os.Args[1]
info, err := os.Stat(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

if info.IsDir() {
err = formatter.FormatDirectory(path)
} else {
err = formatter.FormatFile(path)
}

if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
cli.Execute()
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ go 1.25
require (
github.com/dave/dst v0.27.3
github.com/samber/lo v1.52.0
github.com/spf13/cobra v1.10.2
golang.org/x/mod v0.31.0
gonum.org/v1/gonum v0.16.0
mvdan.cc/gofumpt v0.9.2
)

require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.39.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg=
Expand All @@ -8,6 +9,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
Expand All @@ -18,12 +21,18 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
Expand Down
41 changes: 41 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cli

import (
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/werf/wormatter/pkg/formatter"
)

var version = "dev"

func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

var rootCmd = &cobra.Command{
Use: "wormatter <file.go|directory>",
Short: "A highly opinionated Go source code formatter",
Long: "Wormatter is a DST-based Go source code formatter. Highly opinionated, but very comprehensive. Gofumpt built-in.",
Version: version,
Args: cobra.ExactArgs(1),
RunE: run,
}

func run(_ *cobra.Command, args []string) error {
path := args[0]
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("cannot access %q: %w", path, err)
}

if info.IsDir() {
return formatter.FormatDirectory(path)
}

return formatter.FormatFile(path)
}
169 changes: 169 additions & 0 deletions pkg/formatter/callgraph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package formatter

import (
"github.com/dave/dst"
"github.com/samber/lo"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
)

func buildCallGraph(funcs []*dst.FuncDecl, localFuncs map[string]bool) map[string][]string {
graph := make(map[string][]string)

for _, fn := range funcs {
name := fn.Name.Name
graph[name] = []string{}

if fn.Body == nil {
continue
}

dst.Inspect(fn.Body, func(n dst.Node) bool {
call, ok := n.(*dst.CallExpr)
if !ok {
return true
}
ident, ok := call.Fun.(*dst.Ident)
if !ok {
return true
}
if localFuncs[ident.Name] && ident.Name != name {
graph[name] = append(graph[name], ident.Name)
}

return true
})
}

return graph
}

func assignLayers(callGraph map[string][]string, funcNames map[string]bool) map[string]int {
g := simple.NewDirectedGraph()
nameToID := make(map[string]int64)
idToName := make(map[int64]string)

var nextID int64
for name := range funcNames {
nameToID[name] = nextID
idToName[nextID] = name
g.AddNode(simple.Node(nextID))
nextID++
}

for caller, callees := range callGraph {
for _, callee := range callees {
g.SetEdge(g.NewEdge(simple.Node(nameToID[caller]), simple.Node(nameToID[callee])))
}
}

sccs := topo.TarjanSCC(g)

sccID := make(map[string]int)
for i, scc := range sccs {
for _, node := range scc {
sccID[idToName[node.ID()]] = i
}
}

sccGraph := make(map[int][]int)
for i := range sccs {
sccGraph[i] = []int{}
}
for caller, callees := range callGraph {
callerSCC := sccID[caller]
for _, callee := range callees {
calleeSCC := sccID[callee]
if callerSCC != calleeSCC {
sccGraph[callerSCC] = append(sccGraph[callerSCC], calleeSCC)
}
}
}

sccLayers := make(map[int]int)
var computeSCCLayer func(scc int) int
computeSCCLayer = func(scc int) int {
if layer, ok := sccLayers[scc]; ok {
return layer
}
maxChildLayer := -1
for _, child := range sccGraph[scc] {
childLayer := computeSCCLayer(child)
if childLayer > maxChildLayer {
maxChildLayer = childLayer
}
}
sccLayers[scc] = maxChildLayer + 1

return sccLayers[scc]
}

for i := range sccs {
computeSCCLayer(i)
}

layers := make(map[string]int)
for name := range funcNames {
layers[name] = sccLayers[sccID[name]]
}

return layers
}

func isFuncInterface(iface *dst.InterfaceType) bool {
return iface.Methods != nil && len(iface.Methods.List) == 1 && isFuncType(iface.Methods.List[0].Type)
}

func isFuncType(expr dst.Expr) bool {
_, ok := expr.(*dst.FuncType)

return ok
}

func isBlankVarSpec(spec dst.Spec) bool {
vs, ok := spec.(*dst.ValueSpec)
if !ok {
return false
}

return lo.ContainsBy(vs.Names, func(name *dst.Ident) bool {
return name.Name == "_"
})
}

func hasIota(d *dst.GenDecl) bool {
for _, spec := range d.Specs {
vs, ok := spec.(*dst.ValueSpec)
if !ok {
continue
}
for _, val := range vs.Values {
if containsIota(val) {
return true
}
}
}

return false
}

func containsIota(expr dst.Expr) bool {
switch e := expr.(type) {
case *dst.Ident:
return e.Name == "iota"
case *dst.BinaryExpr:
return containsIota(e.X) || containsIota(e.Y)
case *dst.UnaryExpr:
return containsIota(e.X)
case *dst.ParenExpr:
return containsIota(e.X)
case *dst.CallExpr:
for _, arg := range e.Args {
if containsIota(arg) {
return true
}
}
}

return false
}
Loading