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
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Go Efferent and Afferent package metric calculator.
Metrics calculated currently by this package:

- [x] Instability metric
- [ ] Abstractness metric
- [ ] Distance from main sequence metric
- [x] Abstractness metric
- [x] Distance from main sequence metric

All metrics are now supported and calculated accordingly.

# Description of these metrics

Expand All @@ -17,3 +19,9 @@ https://en.wikipedia.org/wiki/Software_package_metrics
Please see Robert Cecil Martin's Clean Architecture book on details describing those metrics.

In terms of what this tool is doing, please refer to this post: [Efferent and Afferent Metrics in Go](https://skarlso.github.io/2019/04/21/efferent-and-afferent-metrics-in-go/).

# Usage on Effrit

Here is an example of running this tool on this very project:

![screenshot](./img/effrit_package.png)
7 changes: 5 additions & 2 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ func scan(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
packages = pkg.Analyse(packages)
pkg.Display(packages)
packages.GatherDependedOnByCount()
packages.CalculateInstability()
packages.CalculateAbstractnessOfPackages()
packages.CalculateDistance()
packages.Display()
return nil
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/schollz/progressbar/v2 v2.12.0
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/olekukonko/tablewriter v0.0.1
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
)
18 changes: 5 additions & 13 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
Expand All @@ -10,18 +7,13 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/schollz/progressbar/v2 v2.12.0 h1:eTkmx9oWkRqi+rAzSDsreLbwGHvlrLJDR8xzmO0ccDI=
github.com/schollz/progressbar/v2 v2.12.0/go.mod h1:fBI3onORwtNtwCWJHsrXtjE3QnJOtqIZrvr3rDaF7L0=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Binary file added img/effrit_package.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 0 additions & 24 deletions pkg/analyse.go

This file was deleted.

40 changes: 0 additions & 40 deletions pkg/display.go

This file was deleted.

148 changes: 148 additions & 0 deletions pkg/packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package pkg

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"

"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
)

// Package is a single package as determined by go list.
type Package struct {
Name string
FullName string
Imports []string
ImportCount float64
DependedOnByCount float64
Stability float64
Abstractness float64
DistanceFromMedian float64
Dir string
GoFiles []string
}

// Packages represents a collection of packages.
type Packages struct {
packageMap map[string]Package
}

// NewPackages returns a new initialized package collection.
func NewPackages() *Packages {
return &Packages{
packageMap: make(map[string]Package),
}
}

// GatherDependedOnByCount looks for packages which import other packages in the project.
func (pkg *Packages) GatherDependedOnByCount() {
for _, v := range pkg.packageMap {
imports := v.Imports
for _, i := range imports {
if p, ok := pkg.packageMap[i]; ok {
p.DependedOnByCount++
pkg.packageMap[p.FullName] = p
}
}
}
}

// CalculateInstability will calculate the instability metric for all packages.
func (pkg *Packages) CalculateInstability() {
for k, v := range pkg.packageMap {
v.Stability = v.ImportCount / (v.ImportCount + v.DependedOnByCount)
pkg.packageMap[k] = v
}
}

// CalculateDistance will calculate the distance from the main sequence for all packages.
// 0: As far away from the main sequence as possible
// 1: Close as possible
// 0,0: Zone of Pain
// 1,1: Zone of Uselessness
func (pkg *Packages) CalculateDistance() {
for k, v := range pkg.packageMap {
v.DistanceFromMedian = abs(v.Stability + v.Abstractness - 1)
pkg.packageMap[k] = v
}
}

func abs(a float64) float64 {
if a < 0 {
return -a
}
return a
}

// CalculateAbstractnessOfPackages will walk all the files in the package
// and analyses abstractness.
func (pkg *Packages) CalculateAbstractnessOfPackages() {
for k, p := range pkg.packageMap {
funcCount := 0.0
abstractsCount := 0.0
fmt.Printf("Scanning %s go file(s) in package %s.\n", keyName.Sprint(len(p.GoFiles)), keyName.Sprint(p.FullName))
for _, f := range p.GoFiles {
fset := token.NewFileSet()
/* #nosec */
data, err := ioutil.ReadFile(filepath.Join(p.Dir, f))
if err != nil {
log.Fatal(err)
}
node, err := parser.ParseFile(fset, f, data, 0)
if err != nil {
log.Fatal(err)
}
ast.Inspect(node, func(n ast.Node) bool {
// TODO: This needs structs with no receivers counted as interface.
switch n.(type) {
case *ast.FuncDecl:
funcCount++
case *ast.InterfaceType:
abstractsCount++
case *ast.StructType:
// This right now calculates structs towards abstractness.
// I have no easy way to find receivers for structs yet
// so I'm counting all structs towards interfaces. If there are
// implementations in these packages they would even out this count.
abstractsCount++
}
return true
})
} // go files in packages
p.Abstractness = abstractsCount / funcCount
pkg.packageMap[k] = p
} // packages
}

var keyName = color.New(color.FgWhite, color.Bold)
var yellow = color.New(color.FgYellow)
var red = color.New(color.FgRed)
var green = color.New(color.FgGreen, color.Bold)

// Display displays the analysed information in a pretty way...
// TODO: Add multiple display options and Graph generation.
func (pkg *Packages) Display() {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"NAME", "STABILITY", "ABSTRACTNESS", "DISTANCE"})
for _, p := range pkg.packageMap {
c := &color.Color{}
if p.Stability < 0.5 {
c = green
} else if p.Stability >= 0.5 && p.Stability < 1 {
c = yellow
} else if p.Stability == 1 {
c = red
}
stability := fmt.Sprintf("%.1f", p.Stability)
abstractness := fmt.Sprintf("%.1f", p.Abstractness)
distance := fmt.Sprintf("%.1f", p.DistanceFromMedian)
table.Append([]string{p.FullName, c.Sprint(stability), keyName.Sprint(abstractness), keyName.Sprint(distance)})
}
table.Render()
}
37 changes: 5 additions & 32 deletions pkg/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,10 @@ import (
"strings"
)

// Package is a single package as determined by go list.
type Package struct {
Name string
FullName string
Imports []string
ImportCount float64
DependedOnByCount float64
Stability float64
Dir string
GoFiles []string
}

// Scan will scan a project using go list. As go list is running
// in the background, scan will display a waiting indicator.
func Scan(projectName string) (map[string]Package, error) {
packages := make(map[string]Package)
func Scan(projectName string) (*Packages, error) {
pkgs := NewPackages()
// Format: [packageName] = {outSide import count}
c := "go"
args := []string{
Expand All @@ -37,7 +25,7 @@ func Scan(projectName string) (map[string]Package, error) {
fmt.Println("Waiting for go list to finish scanning the project...")
b, err := cmd.Output()
if err != nil {
return packages, err
return pkgs, err
}
lines := bytes.Split(b, []byte("\n"))
for _, line := range lines {
Expand All @@ -50,7 +38,6 @@ func Scan(projectName string) (map[string]Package, error) {
imports := split[1]
goFiles := string(split[2])
dir := split[3]

gf := strings.Split(goFiles, ",")

is := bytes.Split(imports, []byte(","))
Expand All @@ -73,21 +60,7 @@ func Scan(projectName string) (map[string]Package, error) {
p.ImportCount++
}
}
packages[p.FullName] = p
}
packages = gatherDependedOnByCount(packages)
return packages, nil
}

func gatherDependedOnByCount(packages map[string]Package) map[string]Package {
for _, v := range packages {
imports := v.Imports
for _, i := range imports {
if p, ok := packages[i]; ok {
p.DependedOnByCount++
packages[p.FullName] = p
}
}
pkgs.packageMap[p.FullName] = p
}
return packages
return pkgs, nil
}