Skip to content

Commit 20c33eb

Browse files
committed
arch linter with custom analysis and golangci
1 parent 6cc3d4c commit 20c33eb

8 files changed

Lines changed: 185 additions & 2 deletions

File tree

.golangci.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ linters:
88
- funcorder
99
- bodyclose
1010
- contextcheck
11-
- depguard
1211
- dupword
1312
- wrapcheck
1413
- noinlineerr
@@ -35,6 +34,34 @@ linters:
3534
- maintidx
3635
- cyclop
3736
settings:
37+
depguard:
38+
rules:
39+
domain:
40+
files:
41+
- "**/internal/modules/**/domain/*.go"
42+
- "!**/*_test.go"
43+
deny:
44+
- pkg: "github.com/danicc097/todo-ddd-example/internal/modules/*/application"
45+
desc: "Arch violation: Domain layer cannot depend on the Application layer."
46+
- pkg: "github.com/danicc097/todo-ddd-example/internal/modules/*/infrastructure"
47+
desc: "Arch violation: Domain layer cannot depend on the Infrastructure layer."
48+
- pkg: "github.com/danicc097/todo-ddd-example/internal/infrastructure"
49+
desc: "Arch violation: Domain layer cannot depend on global Infrastructure."
50+
- pkg: "github.com/gin-gonic/gin"
51+
desc: "Arch violation: Domain layer cannot know about HTTP routing."
52+
- pkg: "github.com/jackc/pgx"
53+
desc: "Arch violation: Domain layer cannot know about the database."
54+
application:
55+
files:
56+
- "**/internal/modules/**/application/*.go"
57+
- "!**/*_test.go"
58+
deny:
59+
- pkg: "github.com/danicc097/todo-ddd-example/internal/modules/*/infrastructure"
60+
desc: "Arch violation: Application layer cannot depend on the Infrastructure layer."
61+
- pkg: "github.com/gin-gonic/gin"
62+
desc: "Arch violation: Application layer cannot know about HTTP routing."
63+
- pkg: "github.com/jackc/pgx"
64+
desc: "Arch violation: Application layer cannot know about the database."
3865
revive:
3966
rules:
4067
- name: unused-parameter
@@ -136,6 +163,7 @@ linters:
136163
- err113
137164
- errcheck
138165
- errchkjson
166+
- exhaustruct
139167
- funlen
140168
- lll
141169
- nonamedreturns

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ deps:
4848
lint:
4949
go build ./... >/dev/null
5050
go test -c ./tests/e2e/... -tags e2e -o /dev/null
51-
$(GOLINT) run ./... --allow-parallel-runners --fix --config=.golangci.yml --issues-exit-code=0 >/dev/null
51+
@echo ">>> Running custom architectural analyzer..."
52+
go test ./tools/archlint/...
53+
@echo ">>> Running critical linters..."
54+
$(GOLINT) run ./... --allow-parallel-runners --config=.golangci.yml --issues-exit-code=1 --enable-only depguard,exhaustruct
55+
@echo ">>> Running linters and fix"
56+
$(GOLINT) run ./... --allow-parallel-runners --fix --config=.golangci.yml --issues-exit-code=0 >/dev/null || true
5257

5358
dev:
5459
$(AIR) -c .air.toml

internal/infrastructure/http/middleware/validation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ func extractKinOpenApiError(err error, baseLoc []string, detail *[]api.Validatio
193193
}
194194

195195
*detail = append(*detail, api.ValidationError{
196+
Ctx: nil,
196197
Loc: append([]string{}, baseLoc...),
197198
Msg: msg,
198199
Detail: api.ValidationErrorDetail{
@@ -224,6 +225,7 @@ func extractKinOpenApiError(err error, baseLoc []string, detail *[]api.Validatio
224225
}
225226

226227
*detail = append(*detail, api.ValidationError{
228+
Ctx: nil,
227229
Loc: loc,
228230
Msg: schemaErr.Reason,
229231
Detail: api.ValidationErrorDetail{
@@ -241,6 +243,7 @@ func extractKinOpenApiError(err error, baseLoc []string, detail *[]api.Validatio
241243

242244
if len(baseLoc) > 0 {
243245
*detail = append(*detail, api.ValidationError{
246+
Ctx: nil,
244247
Loc: append([]string{}, baseLoc...),
245248
Msg: err.Error(),
246249
Detail: api.ValidationErrorDetail{Value: ""},

internal/modules/todo/infrastructure/http/todo.handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func (h *TodoHandler) mapReadModelToAPI(t application.TodoReadModel) api.Todo {
117117
}
118118

119119
return api.Todo{
120+
CompletionLogs: nil,
120121
Id: t.ID,
121122
WorkspaceId: t.WorkspaceID,
122123
Title: t.Title,

tools/archlint/handler_check.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package archlint
2+
3+
import (
4+
"go/ast"
5+
"go/types"
6+
"strings"
7+
8+
"golang.org/x/tools/go/analysis"
9+
"golang.org/x/tools/go/analysis/passes/inspect"
10+
"golang.org/x/tools/go/ast/inspector"
11+
)
12+
13+
//nolint:gochecknoglobals
14+
var HandlerAnalyzer = &analysis.Analyzer{
15+
Name: "handlerarch",
16+
Doc: "ensures HTTP handlers do not call Repositories directly",
17+
Requires: []*analysis.Analyzer{inspect.Analyzer},
18+
Run: runHandlerArch,
19+
}
20+
21+
//nolint:nilnil
22+
func runHandlerArch(pass *analysis.Pass) (any, error) {
23+
if !strings.Contains(pass.Pkg.Path(), "/infrastructure/http") {
24+
return nil, nil
25+
}
26+
27+
insp, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
28+
if !ok {
29+
return nil, nil
30+
}
31+
32+
nodeFilter := []ast.Node{(*ast.FuncDecl)(nil)}
33+
34+
insp.Preorder(nodeFilter, func(node ast.Node) {
35+
funcDecl, ok := node.(*ast.FuncDecl)
36+
if !ok || funcDecl.Recv == nil || funcDecl.Body == nil {
37+
return
38+
}
39+
40+
recvName := getReceiverName(funcDecl)
41+
if !strings.HasSuffix(recvName, "Handler") {
42+
return
43+
}
44+
45+
inspectBody(pass, funcDecl.Body, recvName)
46+
})
47+
48+
return nil, nil
49+
}
50+
51+
func getReceiverName(funcDecl *ast.FuncDecl) string {
52+
if len(funcDecl.Recv.List) == 0 {
53+
return ""
54+
}
55+
56+
switch exp := funcDecl.Recv.List[0].Type.(type) {
57+
case *ast.StarExpr:
58+
if ident, ok := exp.X.(*ast.Ident); ok {
59+
return ident.Name
60+
}
61+
case *ast.Ident:
62+
return exp.Name
63+
}
64+
65+
return ""
66+
}
67+
68+
func inspectBody(pass *analysis.Pass, body *ast.BlockStmt, recvName string) {
69+
ast.Inspect(body, func(n ast.Node) bool {
70+
call, ok := n.(*ast.CallExpr)
71+
if !ok {
72+
return true
73+
}
74+
75+
sel, ok := call.Fun.(*ast.SelectorExpr)
76+
if !ok {
77+
return true
78+
}
79+
80+
obj := pass.TypesInfo.Uses[sel.Sel]
81+
if obj == nil {
82+
return true
83+
}
84+
85+
checkViolation(pass, obj, call, recvName, sel.Sel.Name)
86+
87+
return true
88+
})
89+
}
90+
91+
func checkViolation(pass *analysis.Pass, obj types.Object, call *ast.CallExpr, recvName, methodName string) {
92+
sig, ok := obj.Type().(*types.Signature)
93+
if !ok || sig.Recv() == nil {
94+
return
95+
}
96+
97+
recvType := sig.Recv().Type()
98+
99+
if ptr, isPtr := recvType.(*types.Pointer); isPtr {
100+
recvType = ptr.Elem()
101+
}
102+
103+
named, ok := recvType.(*types.Named)
104+
if !ok || named.Obj().Pkg() == nil {
105+
return
106+
}
107+
108+
iface := named.Obj().Name()
109+
pkgPath := named.Obj().Pkg().Path()
110+
111+
if strings.Contains(pkgPath, "/domain") && strings.HasSuffix(iface, "Repository") {
112+
pass.Reportf(call.Pos(), "Arch violation: %s calls %s.%s directly. Handlers must route through Application UseCases.", recvName, iface, methodName)
113+
}
114+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package archlint_test
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/tools/go/analysis/analysistest"
7+
8+
"github.com/danicc097/todo-ddd-example/tools/archlint"
9+
)
10+
11+
func TestHandlerArch(t *testing.T) {
12+
t.Parallel()
13+
14+
testdata := analysistest.TestData()
15+
analysistest.Run(t, testdata, archlint.HandlerAnalyzer, "github.com/danicc097/todo-ddd-example/internal/modules/...")
16+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package domain
2+
3+
type TodoRepository interface {
4+
Save()
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package http
2+
3+
import "github.com/danicc097/todo-ddd-example/internal/modules/todo/domain"
4+
5+
type TodoHandler struct {
6+
repo domain.TodoRepository
7+
}
8+
9+
func (h *TodoHandler) Create() {
10+
h.repo.Save() // want "Arch violation: TodoHandler calls TodoRepository.Save directly. Handlers must route through Application UseCases."
11+
}

0 commit comments

Comments
 (0)