|
| 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 | +} |
0 commit comments