Awesome
<h1 align="center"> <img src="https://user-images.githubusercontent.com/19743841/97435499-d7541280-1963-11eb-80e1-70d3d80f30cc.png" /> </h1> <p align="center"> <a href="https://codecov.io/gh/resotto/gochk"><img src="https://codecov.io/gh/resotto/gochk/branch/master/graph/badge.svg?token=0YQPLUGIXA"/></a> <a href="https://github.com/resotto/gochk/actions"><img src="https://github.com/resotto/gochk/workflows/build/badge.svg" /></a> <a href="https://goreportcard.com/report/github.com/resotto/gochk"><img src="https://goreportcard.com/badge/github.com/resotto/gochk"></a> <a href="https://bestpractices.coreinfrastructure.org/projects/4380"><img src="https://bestpractices.coreinfrastructure.org/projects/4380/badge"></a> <a href="https://pkg.go.dev/github.com/resotto/gochk?tab=overview"><img src="https://pkg.go.dev/badge/github.com/resotto/gochk" alt="PkgGoDev"></a> <a href="https://github.com/resotto/gochk/issues/1"><img src="https://img.shields.io/badge/chat-on%20issue-yellow"></a> <a href="https://github.com/resotto/gochk/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" /></a> </p> <p align="center"> Static Dependency Analysis Tool for Go Files </p> <p align="center"> <img src="https://user-images.githubusercontent.com/19743841/97001249-0983ff80-1573-11eb-818f-9bdbffe8f762.gif"> <a href="https://asciinema.org/a/371171" target="_blank"><img src="https://asciinema.org/a/371171.svg" /></a> </p>What is Gochk?
-
Gochk analyzes statically whether .go files violate Clean Architecture The Dependency Rule or not, and prints its results.
This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.
What problem does Gochk solve?
- When Go codebase with Clean Architecture (or Layered Architecture) becomes larger, it might be Big Ball Of Mud by the Dependency Rule violation, and Gochk detects them.
When to apply Gochk to codebase?
- Preferably, it is when the codebase is as small as possible.
- In this phase, you can use Gochk with
-e=true
which means Gochk fails with exit code1
when violations occur. So you can detect violations thoroughly and keep codes clean.
- In this phase, you can use Gochk with
- If codebase is big, Gochk can also be applied to it.
- You can just check whether it violates Dependency Rule or not and refactor it to have less violations.
Who is the main user of Gochk?
- Go Developer
Why Gochk?
- ZERO Dependency
- Simple & Easy-to-Read Outputs
Table of Contents
- Getting Started
- Installation
- How Gochk works
- How to see results
- Configuration
- Customization
- Unit Testing
- Performance Test
- Build
- GitHub Actions
- Feedback
- Contributing
- Release Notes
- License
- Author
Getting Started
Docker
See Build.
Local
go get -u github.com/resotto/gochk
cd ${GOPATH}/src/github.com/resotto/gochk
Please edit paths of dependencyOrders
in gochk/configs/config.json
according to your dependency rule, whose smaller index value means outer circle.
"dependencyOrders": ["external", "adapter", "application", "domain"],
And then, let's gochk your target path with -t
:
go run cmd/gochk/main.go -t=${YourTargetPath}
If you have Goilerplate, you can also gochk it:
go run cmd/gochk/main.go -t=../goilerplate
If your current working directory is not in Gochk root ${GOPATH}/src/github.com/resotto/gochk
, you must specify the location of the config.json
with -c
:
cd internal
go run ../cmd/gochk/main.go -t=../../goilerplate -c=../configs/config.json
Moreover, if you want to exit with 1
when violations occur, please specify -e=true
(default false
):
go run ../cmd/gochk/main.go -t=../../goilerplate -c=../configs/config.json -e=true
Installation
First of all, let's check GOPATH
has already been set:
go env GOPATH
And then, please confirm that ${GOPATH}/bin
is included in your $PATH
:
echo $PATH
Finally, please install Gochk:
cd cmd/gochk
go install
How Gochk works
Prerequisites
- Please format all .go files with one of the following format tools in advance, which means only one import statement in a .go file.
- goimports
- goreturns
- gofumports
- If you have files with following file path or import path, Gochk might not work well.
- The path including the two directory names specified in
dependencyOrders
ofgochk/configs/config.json
.- For example, if you have the path
app/external/adapter/service
and want to handle this path as what is inadapter
, anddependencyOrders = ["external", "adapter"]
, the index of the path will be0
(not1
).
- For example, if you have the path
- The path including the two directory names specified in
What Gochk does
Gochk checks whether .go files violate Clean Architecture The Dependency Rule or not, and prints its results.
This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle.
For example, if an usecase in "Use Cases" imports (depends on) what is in "Controllers/Gateways/Presenters", it violates dependency rule.
<p align="center"> <img src="https://user-images.githubusercontent.com/19743841/93830264-afa9c480-fcaa-11ea-9589-7c5308c291f4.jpg"> </p> <p align="center"> <a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">The Clean Architecture</a> </p>Check Logic
Firstly, Gochk fetches the file path and gets the index of dependencyOrders
in gochk/configs/config.json
if one of them is included in the file path.
Secondly, Gochk reads the file, parses import paths, and also gets the indices of dependencyOrders
if matched.
And then, Gochk compares those indices and detects violation if the index of the import path is smaller than that of the file path.
For example, if you have a file app/application/usecase/xxx.go
with import path "app/adapter/service"
and dependencyOrders = ["adapter", "application"]
, the index of the file is 1
and the index of its import is 0
.
Therefore, the file violates dependency rule since the following inequality is established:
0 (the index of the import path) < 1 (the index of the file path)
How to see results
Quick Check
You can check whether there are violations or not quickly by looking at the end of results.
If you see Dependencies which violate dependency orders found!
, there are violations!π¨
2020/10/19 23:37:03 Dependencies which violate dependency orders found!
If you see the following AA, congrats! there are no violationsπ
2020/10/19 23:57:25 No violations
________ _______ ______ __ __ __ _ _
/ ______\ / ___ \ / ____\ | | | | | | / /
/ / ____ / / \ \ / / | |___| | | |/ /
/ / |_ | | | | | | | | ___ | | /
\ \ \ | | | | | | | | | | | | |\ \
\ \___/ / \ \___/ / \ \_____ | | | | | | \ \
\_______/ \_______/ \_______\ |__| |__| |__| \__\
Result types
Gochk displays each result type in a different color by default:
- None
- which means there are imports irrelevant to dependency rule or no imports at all.
- Verified
- which means there are dependencies with no violation.
- Ignored
- which means the path is ignored (not checked).
- Warning
- which means something happened (and Gochk didn't check it).
- Violated
- which means there are dependencies which violates dependency rule.
For None
, Verified
, and Ignored
, only the file path will be displayed.
[None] ../goilerplate/internal/app/adapter/postgresql/conn.go
[Verified] ../goilerplate/cmd/app/main.go
[Ignored] ../goilerplate/.git
For Warning
, it displays what happened to the file.
[Warning] open /Users/resotto/go/src/github.com/resotto/goilerplate/internal/app/application/usecase/lock.go: permission denied
For Violated
, it displays the file path, its dependency, and how it violates dependency rule.
[Violated] ../goilerplate/internal/app/domain/temp.go imports "github.com/resotto/goilerplate/internal/app/adapter/postgresql/model"
=> domain depends on adapter
Configuration
Changing Default Target Path, Config Path, and Exit Mode
You can modify default target path, config path, and exit mode in main.go:
exitMode := flag.Bool("e", false /* default value */, "flag whether Gochk exits with 1 or not when violations occur. (false is default)")
targetPath := flag.String("t", "." /* default value */, "target path (\".\" is default)")
configPath := flag.String("c", "configs/config.json" /* default value */, "configuration file path (\"configs/config.json\" is default)")
config.json
gochk/configs/config.json
has configuration values.
{
"dependencyOrders": ["external", "adapter", "application", "domain"],
"ignore": ["test", ".git"],
"printViolationsAtTheBottom": false
}
-
dependencyOrders
are the paths of each circles in Clean Architecture.-
For example, if you have following four circles, you should specify them from the outer to the inner like:
["external", "adapter", "application", "domain"]
.- "External" (most outer)
- "Adapter"
- "Application"
- "Domain" (most inner, the core)
-
If you have other layered architecture, you could specify its layers to this parameter as well.
-
-
ignore
has the paths ignored by Gochk, which can be file path or dir path.- If you have the directory you want to ignore, specifying them might improve the performance of Gochk since it returns
filepath.SkipDir
.
- If you have the directory you want to ignore, specifying them might improve the performance of Gochk since it returns
// read.go
func matchIgnore(ignorePaths []string, path string, info os.FileInfo) (bool, error) {
if included, _ := include(ignorePaths, path); included {
if info.IsDir() {
return true, filepath.SkipDir
}
return true, nil
}
return false, nil
}
-
printViolationsAtTheBottom
is the flag whether Gochk prints violations of the dependency rule at the bottom or not.- If
true
, you can see violations at the bottom like:
- If
false
, you see them disorderly (by goroutine):
- If
Customization
Changing Result Color
First, please add the ANSI escape code to print.go:
const (
teal color = "\033[1;36m"
green = "\033[1;32m"
yellow = "\033[1;33m"
purple = "\033[1;35m"
red = "\033[1;31m"
newColor = "\033[1;34m" // New color
reset = "\033[0m"
)
And then, let's change color of result type in read.go:
func newWarning(message string) CheckResult {
cr := CheckResult{}
cr.resultType = warning
cr.message = message
cr.color = newColor // New color
return cr
}
Tuning the Number of Goroutine
If printViolationsAtTheBottom
is false
, Gochk prints results with goroutine.
You can change the number of goroutine in print.go:
func printConcurrently(results []CheckResult) {
c := make(chan struct{}, 10) // 10 goroutines by default
var wg sync.WaitGroup
for _, r := range results {
r := r
c <- struct{}{}
wg.Add(1)
go func() {
defer func() { <-c; wg.Done() }()
printColorMessage(r)
}()
}
wg.Wait()
}
Unit Testing
Unit test files are located in gochk/internal/gochk
.
gochk
βββ internal
βΒ Β βββ gochk
βΒ Β βββ calc_internal_test.go # Unit test (internal)
βΒ Β βββ read_internal_test.go # Unit test (internal)
βββ test
βββ testdata # Test data
So you can do unit test like:
~/go/src/github.com/resotto/gochk (master) > go test ./internal/gochk/... # Please specify -v if you need detailed outputs
ok github.com/resotto/gochk/internal/gochk (cached)
You can also clean test cache with go clean -testcache
.
~/go/src/github.com/resotto/gochk (master) > go clean -testcache
~/go/src/github.com/resotto/gochk (master) > go test ./internal/gochk/...
ok github.com/resotto/gochk/internal/gochk 0.092s # Not cache
Performance Test
Performance test file is located in gochk/test/performance
.
gochk
βββ test
βββ performance
βββ check_test.go # Performance test
Thus, you can do performance test as follows. It will take few minutes.
~/go/src/github.com/resotto/gochk (master) > go test ./test/performance/...
ok github.com/resotto/gochk/test/performance 61.705s
Test Contents
Performance test checks 40,000 test files in gochk/test/performance
and measures only how long it takes to do it.
Note
- Test files will be created before the test and be deleted after the test.
- For each test directory, there will be 10,000 .go test files.
gochk
βββ test
βββ performance
β βββ adapter # Test directory
β β βββ postgresql
β β β βββ model
β β βββ repository
β β βββ service
β β βββ view
β β ... # Test files (g0.go ~ g9999.go)
β βββ application # Test directory
β β βββ service
β β βββ usecase
β β ... # Test files (g0.go ~ g9999.go)
β βββ domain # Test directory
β β βββ factory
β β βββ repository
β β βββ valueobject
β β ... # Test files (g0.go ~ g9999.go)
β βββ external # Test directory
β ... # Test files (g0.go ~ g9999.go)
βββ testdata
Β Β βββ adapter.txt # Original file of performance/adapter/gX.go
Β Β βββ application.txt # Original file of performance/application/gX.go
Β Β βββ domain.txt # Original file of performance/domain/gX.go
Β Β βββ external.txt # Original file of performance/external/gX.go
For each file, it imports standard libraries and dependencies like:
package xxx
import (
// standard library imports omitted here
"github.com/resotto/gochk/test/performance/adapter" // import this up to adapter
"github.com/resotto/gochk/test/performance/adapter/postgresql" // import this up to adapter
"github.com/resotto/gochk/test/performance/adapter/postgresql/model" // import this up to adapter
"github.com/resotto/gochk/test/performance/adapter/repository" // import this up to adapter
"github.com/resotto/gochk/test/performance/adapter/service" // import this up to adapter
"github.com/resotto/gochk/test/performance/adapter/view" // import this up to adapter
"github.com/resotto/gochk/test/performance/application/service" // import this up to application
"github.com/resotto/gochk/test/performance/application/usecase" // import this up to application
"github.com/resotto/gochk/test/performance/domain/factory" // import this only in domain
"github.com/resotto/gochk/test/performance/domain/repository" // import this only in domain
"github.com/resotto/gochk/test/performance/domain/valueobject" // import this only in domain
"github.com/resotto/gochk/test/performance/external" // import this up to adapter
)
In performance test, dependencyOrders
are:
var dependencyOrders = []string{"external", "adapter", "application", "domain"}
So, the number of violations equals to:
-
domain
- there are 9 violations x 10,000 files = 90,000
- domain depends on application (x2)
"github.com/resotto/gochk/test/performance/application/service" "github.com/resotto/gochk/test/performance/application/usecase"
- domain depends on adapter (x6)
"github.com/resotto/gochk/test/performance/adapter" "github.com/resotto/gochk/test/performance/adapter/postgresql" "github.com/resotto/gochk/test/performance/adapter/postgresql/model" "github.com/resotto/gochk/test/performance/adapter/repository" "github.com/resotto/gochk/test/performance/adapter/service" "github.com/resotto/gochk/test/performance/adapter/view"
- domain depends on external (x1)
"github.com/resotto/gochk/test/performance/external"
- domain depends on application (x2)
- there are 9 violations x 10,000 files = 90,000
-
application
- there are 7 violations x 10,000 files = 70,000
- application depends on adapter (x6)
"github.com/resotto/gochk/test/performance/adapter" "github.com/resotto/gochk/test/performance/adapter/postgresql" "github.com/resotto/gochk/test/performance/adapter/postgresql/model" "github.com/resotto/gochk/test/performance/adapter/repository" "github.com/resotto/gochk/test/performance/adapter/service" "github.com/resotto/gochk/test/performance/adapter/view"
- application depends on external (x1)
"github.com/resotto/gochk/test/performance/external"
- application depends on adapter (x6)
- there are 7 violations x 10,000 files = 70,000
-
adapter
- there is 1 violation x 10,000 files = 10,000
- adapter depends on external (x1)
"github.com/resotto/gochk/test/performance/external"
- adapter depends on external (x1)
- there is 1 violation x 10,000 files = 10,000
-
external
- there are no violations.
-
Total
- 90,000 (domain) + 70,000 (application) + 10,000 (adapter) = 170,000 violations
Score
Following scores are not cached ones and measured by two Macbook Pros whose spec is different.
CPU | RAM | 1st score | 2nd score | 3rd score | Average |
---|---|---|---|---|---|
2.7 GHz Dual-Core Intel Core i5 | 8 GB 1867 MHz DDR3 | 99.53s | 97.08s | 93.88s | 96.83s |
2 GHz Quad-Core Intel Core i5 | 32 GB 3733 MHz LPDDR4X | 59.64s | 55.57s | 52.09s | 55.77s |
Build
From Gochk root directory ${GOPATH}/src/github.com/resotto/gochk
, please run:
docker build -t gochk:latest -f build/Dockerfile .
Or you can also pull the image from GitHub Container Registry:
docker pull ghcr.io/resotto/gochk:latest
After getting Gochk docker image, please prepare Dockerfile with the package you want to gochk:
# FROM gochk:latest
FROM ghcr.io/resotto/gochk:latest
RUN go get -u ${TargetPackage}
WORKDIR /go/src/github.com/resotto/gochk
ENTRYPOINT ["/go/bin/gochk", "-t=${TargetPackageRoot}"]
And then, please build the docker image:
docker build -t gochk-${YourPackage}:latest .
Finally, let's gochk your target package on docker container:
docker run --rm -it gochk-${YourPackage}:latest
GitHub Actions
You can gochk your package on GitHub Actions with following yml file:
name: gochk sample
on: [push]
jobs:
gochk-goilerplate:
runs-on: ubuntu-latest
container:
image: docker://ghcr.io/resotto/gochk:latest
steps:
- name: Clone Goilerplate
uses: actions/checkout@v2
with:
repository: resotto/goilerplate
- name: Run gochk
run: |
/go/bin/gochk -c=/go/src/github.com/resotto/gochk/configs/config.json
<p align="center">
<img src="https://user-images.githubusercontent.com/19743841/97106961-1ab24500-1708-11eb-939d-275b08744a9f.png">
</p>
Feedback
- Feel free to write your thoughts.
- Report a bug to Bug report.
Contributing
<a href="https://www.contributor-covenant.org/version/2/0/code_of_conduct/"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" /></a>
See Contribution Guide.
Release Notes
License
Author
Resotto
<a href="https://github.com/resotto"><img src="https://user-images.githubusercontent.com/19743841/97778118-4629a980-1bb8-11eb-97ed-76dcdbe50406.png" /></a> <a href="https://twitter.com/resotto3"><img src="https://user-images.githubusercontent.com/19743841/97777698-52f8ce00-1bb5-11eb-93c9-b06e0c48b693.png" /></a> <a href="https://github.com/sponsors/resotto"><img src="https://img.shields.io/badge/Sponsor-ffffff?logo=github&logoColor=pink&style=flat-square" /></a>