From 3c8d4672eb5edc823397746ed02b518e882c4856 Mon Sep 17 00:00:00 2001 From: vshulcz Date: Sun, 14 Dec 2025 17:25:02 +0300 Subject: [PATCH 1/6] feat: staticlint added --- .github/workflows/lint.yml | 44 +++++--- cmd/staticlint/main.go | 101 ++++++++++++++++++ cmd/staticlint/osexitmain/osexitmain.go | 75 +++++++++++++ go.mod | 9 ++ go.sum | 28 +++++ internal/adapters/audit/file/file.go | 2 + internal/adapters/audit/file/writer.go | 2 +- internal/adapters/audit/file/writer_test.go | 2 +- internal/adapters/audit/remote/client.go | 2 +- internal/adapters/audit/remote/client_test.go | 2 +- internal/adapters/audit/remote/remote.go | 2 + .../adapters/collector/runtime/runtime.go | 1 + internal/adapters/http/ginserver/ginserver.go | 2 + internal/adapters/http/ginserver/handler.go | 6 +- .../http/ginserver/middlewares/middlewares.go | 2 + internal/adapters/persistence/file/file.go | 1 + .../adapters/publisher/httpjson/client.go | 10 +- .../adapters/publisher/httpjson/httpjson.go | 2 + internal/adapters/repository/memory/memory.go | 1 + .../adapters/repository/postgres/postgres.go | 1 + internal/config/config.go | 2 + internal/domain/domain.go | 2 + internal/misc/misc.go | 2 + internal/ports/ports.go | 2 + internal/services/agent/agent.go | 1 + internal/services/audit/audit.go | 2 + internal/services/metrics/metrics.go | 1 + pkg/observer/observer.go | 2 + 28 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 cmd/staticlint/main.go create mode 100644 cmd/staticlint/osexitmain/osexitmain.go create mode 100644 internal/adapters/audit/file/file.go create mode 100644 internal/adapters/audit/remote/remote.go create mode 100644 internal/adapters/http/ginserver/ginserver.go create mode 100644 internal/adapters/http/ginserver/middlewares/middlewares.go create mode 100644 internal/adapters/publisher/httpjson/httpjson.go create mode 100644 internal/config/config.go create mode 100644 internal/domain/domain.go create mode 100644 internal/misc/misc.go create mode 100644 internal/ports/ports.go create mode 100644 internal/services/audit/audit.go create mode 100644 pkg/observer/observer.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 74d7371..38f9465 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,19 +1,33 @@ name: lint on: - pull_request: - push: - branches: [ main ] + pull_request: + push: + branches: [main] jobs: - golangci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - version: latest - args: --timeout=5m + golangci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: latest + args: --timeout=5m + + staticlint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - name: Install dependencies + run: go mod tidy + - name: Run Staticlint + run: go run ./cmd/staticlint/main.go ./... diff --git a/cmd/staticlint/main.go b/cmd/staticlint/main.go new file mode 100644 index 0000000..4ad06ab --- /dev/null +++ b/cmd/staticlint/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/multichecker" + + "golang.org/x/tools/go/analysis/passes/assign" + "golang.org/x/tools/go/analysis/passes/atomic" + "golang.org/x/tools/go/analysis/passes/bools" + "golang.org/x/tools/go/analysis/passes/buildtag" + "golang.org/x/tools/go/analysis/passes/cgocall" + "golang.org/x/tools/go/analysis/passes/composite" + "golang.org/x/tools/go/analysis/passes/copylock" + "golang.org/x/tools/go/analysis/passes/errorsas" + "golang.org/x/tools/go/analysis/passes/httpresponse" + "golang.org/x/tools/go/analysis/passes/loopclosure" + "golang.org/x/tools/go/analysis/passes/lostcancel" + "golang.org/x/tools/go/analysis/passes/nilfunc" + "golang.org/x/tools/go/analysis/passes/printf" + "golang.org/x/tools/go/analysis/passes/shift" + "golang.org/x/tools/go/analysis/passes/stdmethods" + "golang.org/x/tools/go/analysis/passes/structtag" + "golang.org/x/tools/go/analysis/passes/tests" + "golang.org/x/tools/go/analysis/passes/unmarshal" + "golang.org/x/tools/go/analysis/passes/unreachable" + "golang.org/x/tools/go/analysis/passes/unsafeptr" + "golang.org/x/tools/go/analysis/passes/unusedresult" + + "honnef.co/go/tools/staticcheck" + "honnef.co/go/tools/stylecheck" + + "github.com/gostaticanalysis/forcetypeassert" + "github.com/gostaticanalysis/nilerr" + "github.com/vshulcz/Golectra/cmd/staticlint/osexitmain" +) + +func main() { + var analyzers []*analysis.Analyzer + + analyzers = append(analyzers, + assign.Analyzer, + atomic.Analyzer, + bools.Analyzer, + buildtag.Analyzer, + cgocall.Analyzer, + composite.Analyzer, + copylock.Analyzer, + errorsas.Analyzer, + httpresponse.Analyzer, + loopclosure.Analyzer, + lostcancel.Analyzer, + nilfunc.Analyzer, + printf.Analyzer, + shift.Analyzer, + stdmethods.Analyzer, + structtag.Analyzer, + tests.Analyzer, + unmarshal.Analyzer, + unreachable.Analyzer, + unsafeptr.Analyzer, + unusedresult.Analyzer, + ) + + for _, a := range staticcheck.Analyzers { + if a == nil || a.Analyzer == nil { + continue + } + if strings.HasPrefix(a.Analyzer.Name, "SA") { + analyzers = append(analyzers, a.Analyzer) + } + } + + var st1000 *analysis.Analyzer + for _, la := range stylecheck.Analyzers { + if la != nil && la.Analyzer != nil && la.Analyzer.Name == "ST1000" { + st1000 = la.Analyzer + break + } + } + if st1000 != nil { + analyzers = append(analyzers, st1000) + } + + analyzers = append(analyzers, nilerr.Analyzer, forcetypeassert.Analyzer, osexitmain.Analyzer) + + multichecker.Main( + filterAnalyzers(analyzers)..., + ) +} + +func filterAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { + var filtered []*analysis.Analyzer + for _, a := range analyzers { + if strings.HasPrefix(a.Name, "Golectra") { + filtered = append(filtered, a) + } + } + return filtered +} diff --git a/cmd/staticlint/osexitmain/osexitmain.go b/cmd/staticlint/osexitmain/osexitmain.go new file mode 100644 index 0000000..975aaca --- /dev/null +++ b/cmd/staticlint/osexitmain/osexitmain.go @@ -0,0 +1,75 @@ +// Package osexitmain defines an analyzer that reports direct calls to os.Exit in the main.main function. +package osexitmain + +import ( + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer is the osexitmain analyzer. +var Analyzer = &analysis.Analyzer{ + Name: "osexitmain", + Doc: "reports direct os.Exit calls in main.main", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +// run is the main function of the analyzer. +func run(pass *analysis.Pass) (any, error) { + if pass.Pkg == nil || pass.Pkg.Name() != "main" { + return nil, nil + } + + insp, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if !ok { + return nil, fmt.Errorf("failed to assert type: expected *inspector.Inspector") + } + + insp.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) { + fd, ok := n.(*ast.FuncDecl) + if !ok { + return + } + if fd.Recv != nil || fd.Name == nil || fd.Name.Name != "main" || fd.Body == nil { + return + } + + ast.Inspect(fd.Body, func(nn ast.Node) bool { + switch x := nn.(type) { + case *ast.FuncLit: + return false + case *ast.CallExpr: + if isOsExitCall(pass, x) { + pass.Reportf(x.Lparen, "It is forbidden to call os.Exit directly in main function; use return code from main instead") + } + } + return true + }) + }) + + return nil, nil +} + +// isOsExitCall checks if the given call expression is a call to os.Exit. +func isOsExitCall(pass *analysis.Pass, call *ast.CallExpr) bool { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || sel.Sel == nil { + return false + } + + if pass.TypesInfo == nil { + return false + } + obj := pass.TypesInfo.Uses[sel.Sel] + fn, ok := obj.(*types.Func) + if !ok || fn.Pkg() == nil { + return false + } + + return fn.Pkg().Path() == "os" && fn.Name() == "Exit" +} diff --git a/go.mod b/go.mod index 99e5a96..94b4a0f 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,19 @@ go 1.25 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/gin-gonic/gin v1.10.1 + github.com/gostaticanalysis/forcetypeassert v0.2.0 + github.com/gostaticanalysis/nilerr v0.1.2 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 github.com/lib/pq v1.10.9 github.com/pressly/goose/v3 v3.25.0 github.com/shirou/gopsutil/v3 v3.24.5 go.uber.org/zap v1.27.0 + golang.org/x/tools v0.36.0 + honnef.co/go/tools v0.6.1 ) require ( + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -23,6 +28,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -43,10 +49,13 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 58f3187..c0b65bb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= @@ -35,6 +37,16 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= +github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= +github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= +github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= +github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= +github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4 h1:d2/eIbH9XjD1fFwD5SHv8x168fjbQ9PB8hvs8DSEC08= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -59,6 +71,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -86,6 +100,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -108,6 +126,10 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= @@ -121,6 +143,10 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= @@ -129,6 +155,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= diff --git a/internal/adapters/audit/file/file.go b/internal/adapters/audit/file/file.go new file mode 100644 index 0000000..7f184a2 --- /dev/null +++ b/internal/adapters/audit/file/file.go @@ -0,0 +1,2 @@ +// Package file provides file-based audit logging functionality. +package file diff --git a/internal/adapters/audit/file/writer.go b/internal/adapters/audit/file/writer.go index 6766108..1a6caae 100644 --- a/internal/adapters/audit/file/writer.go +++ b/internal/adapters/audit/file/writer.go @@ -1,4 +1,4 @@ -package fileaudit +package file import ( "context" diff --git a/internal/adapters/audit/file/writer_test.go b/internal/adapters/audit/file/writer_test.go index 3f8264b..5233cdc 100644 --- a/internal/adapters/audit/file/writer_test.go +++ b/internal/adapters/audit/file/writer_test.go @@ -1,4 +1,4 @@ -package fileaudit +package file import ( "context" diff --git a/internal/adapters/audit/remote/client.go b/internal/adapters/audit/remote/client.go index ad6a2ff..7987b64 100644 --- a/internal/adapters/audit/remote/client.go +++ b/internal/adapters/audit/remote/client.go @@ -1,4 +1,4 @@ -package remoteaudit +package remote import ( "bytes" diff --git a/internal/adapters/audit/remote/client_test.go b/internal/adapters/audit/remote/client_test.go index e853387..7f9da19 100644 --- a/internal/adapters/audit/remote/client_test.go +++ b/internal/adapters/audit/remote/client_test.go @@ -1,4 +1,4 @@ -package remoteaudit +package remote import ( "context" diff --git a/internal/adapters/audit/remote/remote.go b/internal/adapters/audit/remote/remote.go new file mode 100644 index 0000000..028ec0f --- /dev/null +++ b/internal/adapters/audit/remote/remote.go @@ -0,0 +1,2 @@ +// Package remote provides remote HTTP-based audit logging functionality. +package remote diff --git a/internal/adapters/collector/runtime/runtime.go b/internal/adapters/collector/runtime/runtime.go index 80564ce..b6cd880 100644 --- a/internal/adapters/collector/runtime/runtime.go +++ b/internal/adapters/collector/runtime/runtime.go @@ -1,3 +1,4 @@ +// Package runtime implements a metrics collector that samples Go runtime stats and host CPU/RAM usage. package runtime import ( diff --git a/internal/adapters/http/ginserver/ginserver.go b/internal/adapters/http/ginserver/ginserver.go new file mode 100644 index 0000000..6840ef9 --- /dev/null +++ b/internal/adapters/http/ginserver/ginserver.go @@ -0,0 +1,2 @@ +// Package ginserver provides an HTTP server implementation using the Gin framework. +package ginserver diff --git a/internal/adapters/http/ginserver/handler.go b/internal/adapters/http/ginserver/handler.go index 065d8db..4a44290 100644 --- a/internal/adapters/http/ginserver/handler.go +++ b/internal/adapters/http/ginserver/handler.go @@ -3,6 +3,7 @@ package ginserver import ( "encoding/json" "errors" + "fmt" "io" "net/http" "strconv" @@ -33,7 +34,10 @@ var metricsBatchPool = sync.Pool{ } func decodeMetricsBatch(r io.Reader) ([]domain.Metrics, func(), error) { - buf := metricsBatchPool.Get().(*[]domain.Metrics) + buf, ok := metricsBatchPool.Get().(*[]domain.Metrics) + if !ok { + return nil, func() {}, fmt.Errorf("failed to assert type: expected *[]domain.Metrics") + } items := (*buf)[:0] dec := json.NewDecoder(r) dec.DisallowUnknownFields() diff --git a/internal/adapters/http/ginserver/middlewares/middlewares.go b/internal/adapters/http/ginserver/middlewares/middlewares.go new file mode 100644 index 0000000..6a662ea --- /dev/null +++ b/internal/adapters/http/ginserver/middlewares/middlewares.go @@ -0,0 +1,2 @@ +// Package middlewares contains HTTP middlewares for the Gin server. +package middlewares diff --git a/internal/adapters/persistence/file/file.go b/internal/adapters/persistence/file/file.go index 4969c4b..64239f8 100644 --- a/internal/adapters/persistence/file/file.go +++ b/internal/adapters/persistence/file/file.go @@ -1,3 +1,4 @@ +// Package file implements a filesystem-based metrics snapshot persister. package file import ( diff --git a/internal/adapters/publisher/httpjson/client.go b/internal/adapters/publisher/httpjson/client.go index 6f9426d..9bd1635 100644 --- a/internal/adapters/publisher/httpjson/client.go +++ b/internal/adapters/publisher/httpjson/client.go @@ -185,9 +185,15 @@ func (p *compressedPayload) Release() { } func gzipBytes(src []byte) (*compressedPayload, error) { - buf := bufferPool.Get().(*bytes.Buffer) + buf, ok := bufferPool.Get().(*bytes.Buffer) + if !ok { + return nil, fmt.Errorf("failed to assert type: expected *bytes.Buffer") + } buf.Reset() - zw := gzipWriterPool.Get().(*gzip.Writer) + zw, ok := gzipWriterPool.Get().(*gzip.Writer) + if !ok { + return nil, fmt.Errorf("failed to assert type: expected *gzip.Writer") + } zw.Reset(buf) if _, err := zw.Write(src); err != nil { _ = zw.Close() diff --git a/internal/adapters/publisher/httpjson/httpjson.go b/internal/adapters/publisher/httpjson/httpjson.go new file mode 100644 index 0000000..ef9a81a --- /dev/null +++ b/internal/adapters/publisher/httpjson/httpjson.go @@ -0,0 +1,2 @@ +// Package httpjson provides an HTTP JSON publisher implementation. +package httpjson diff --git a/internal/adapters/repository/memory/memory.go b/internal/adapters/repository/memory/memory.go index 6f9acba..1ffce8a 100644 --- a/internal/adapters/repository/memory/memory.go +++ b/internal/adapters/repository/memory/memory.go @@ -1,3 +1,4 @@ +// Package memory implements an in-memory metrics repository. package memory import ( diff --git a/internal/adapters/repository/postgres/postgres.go b/internal/adapters/repository/postgres/postgres.go index 5321ac7..193bd61 100644 --- a/internal/adapters/repository/postgres/postgres.go +++ b/internal/adapters/repository/postgres/postgres.go @@ -1,3 +1,4 @@ +// Package postgres implements a Postgres-backed metrics repository. package postgres import ( diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0055170 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,2 @@ +// Package config provides configuration settings for the application. +package config diff --git a/internal/domain/domain.go b/internal/domain/domain.go new file mode 100644 index 0000000..53862bf --- /dev/null +++ b/internal/domain/domain.go @@ -0,0 +1,2 @@ +// Package domain defines the core domain models and interfaces for the application. +package domain diff --git a/internal/misc/misc.go b/internal/misc/misc.go new file mode 100644 index 0000000..e371c26 --- /dev/null +++ b/internal/misc/misc.go @@ -0,0 +1,2 @@ +// Package misc provides miscellaneous utility functions. +package misc diff --git a/internal/ports/ports.go b/internal/ports/ports.go new file mode 100644 index 0000000..eb4b0f3 --- /dev/null +++ b/internal/ports/ports.go @@ -0,0 +1,2 @@ +// Package ports defines the interfaces for the application's input and output boundaries. +package ports diff --git a/internal/services/agent/agent.go b/internal/services/agent/agent.go index 1f0a376..a514b08 100644 --- a/internal/services/agent/agent.go +++ b/internal/services/agent/agent.go @@ -1,3 +1,4 @@ +// Package agent implements the metrics collection agent. package agent import ( diff --git a/internal/services/audit/audit.go b/internal/services/audit/audit.go new file mode 100644 index 0000000..84b6f47 --- /dev/null +++ b/internal/services/audit/audit.go @@ -0,0 +1,2 @@ +// Package audit provides functionalities for auditing user actions within the application. +package audit diff --git a/internal/services/metrics/metrics.go b/internal/services/metrics/metrics.go index 61f01b2..9589f43 100644 --- a/internal/services/metrics/metrics.go +++ b/internal/services/metrics/metrics.go @@ -1,3 +1,4 @@ +// Package metrics implements business logic for managing application metrics. package metrics import ( diff --git a/pkg/observer/observer.go b/pkg/observer/observer.go new file mode 100644 index 0000000..2e3da8c --- /dev/null +++ b/pkg/observer/observer.go @@ -0,0 +1,2 @@ +// Package observer implements the Observer design pattern. +package observer From 27f1eccf4e6940ddc189b1cb71a9740087f7ca92 Mon Sep 17 00:00:00 2001 From: vshulcz Date: Sun, 14 Dec 2025 22:08:05 +0300 Subject: [PATCH 2/6] feat: reset generator --- .golangci.yml | 147 +++++++------ cmd/reset/main.go | 525 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 597 insertions(+), 75 deletions(-) create mode 100644 cmd/reset/main.go diff --git a/.golangci.yml b/.golangci.yml index 8fdd85d..410c122 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,84 +1,81 @@ version: "2" run: - timeout: 5m - tests: true - concurrency: 0 + timeout: 5m + tests: true + concurrency: 0 linters: - default: none - enable: - - govet - - staticcheck - - errcheck - - ineffassign - - unused - - revive - - gocyclo - - dupl - - goconst - - gocritic - - misspell - - prealloc - - copyloopvar - - nakedret - - unparam - - gosec - - noctx - - sqlclosecheck - - nolintlint - settings: - gocyclo: - min-complexity: 15 - dupl: - threshold: 150 - goconst: - min-len: 2 - min-occurrences: 3 - misspell: - locale: US - gocritic: - enabled-checks: - - rangeValCopy - - appendCombine - - captLocal - - ifElseChain - - sloppyLen - nolintlint: - require-explanation: true - require-specific: true - revive: - rules: - - name: exported - disabled: false - exclusions: - paths: - - vendor - - third_party - - gen - - generated - - ".*_generated\\.go" - - ".*\\.pb\\.go" - - ".*_mock\\.go" - rules: - - path: '(.+)_test\.go' - linters: - - unparam - - gocyclo - - dupl - - gosec - - linters: [errcheck] - text: "Error return value of .*\\.(Close|Stop)\\(\\) is not checked" - - text: "context.WithCancel function results are not used" - source: "context\\.WithCancel\\(" + default: none + enable: + - govet + - staticcheck + - errcheck + - ineffassign + - unused + - revive + - gocyclo + - dupl + - goconst + - gocritic + - misspell + - prealloc + - copyloopvar + - nakedret + - unparam + - gosec + - noctx + - sqlclosecheck + - nolintlint + settings: + gocyclo: + min-complexity: 15 + dupl: + threshold: 150 + goconst: + min-len: 2 + min-occurrences: 3 + misspell: + locale: US + gocritic: + enabled-checks: + - rangeValCopy + - appendCombine + nolintlint: + require-explanation: true + require-specific: true + revive: + rules: + - name: exported + disabled: false + exclusions: + paths: + - vendor + - third_party + - gen + - generated + - ".*_generated\\.go" + - ".*\\.pb\\.go" + - ".*_mock\\.go" + rules: + - path: '(.+)_test\.go' + linters: + - unparam + - gocyclo + - dupl + - gosec + - linters: [errcheck] + text: "Error return value of .*\\.(Close|Stop)\\(\\) is not checked" + - text: "context.WithCancel function results are not used" + source: "context\\.WithCancel\\(" issues: - max-issues-per-linter: 0 - max-same-issues: 0 + max-issues-per-linter: 0 + max-same-issues: 0 formatters: - enable: - - gofumpt - settings: - gofumpt: - extra-rules: true + enable: + - gofumpt + settings: + gofumpt: + extra-rules: true diff --git a/cmd/reset/main.go b/cmd/reset/main.go new file mode 100644 index 0000000..c8f8200 --- /dev/null +++ b/cmd/reset/main.go @@ -0,0 +1,525 @@ +// Command reset generates Reset() methods for structs marked with "// generate:reset" comment. +// It supports basic types, slices, maps, pointers, and nested structs. +// For structs from other packages, it assigns zero values instead of resetting fields. +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "golang.org/x/tools/go/packages" +) + +const marker = "generate:reset" + +func main() { + root, err := findModuleRoot() + if err != nil { + log.Fatal(err) + } + + cfg := &packages.Config{ + Dir: root, + Mode: packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedSyntax | + packages.NeedTypes | + packages.NeedTypesInfo, + } + + pkgs, err := packages.Load(cfg, "./...") + if err != nil { + log.Fatal(err) + } + + for _, p := range pkgs { + if len(p.Errors) > 0 { + var b strings.Builder + fmt.Fprintf(&b, "package load errors:\n") + for _, e := range p.Errors { + fmt.Fprintf(&b, " - %s\n", e) + } + log.Fatal(b.String()) + } + } + + changed := 0 + for _, p := range pkgs { + if len(p.CompiledGoFiles) == 0 { + continue + } + + targets := findTargetStructs(p) + outPath := filepath.Join(filepath.Dir(p.CompiledGoFiles[0]), "reset.gen.go") + + if len(targets) == 0 { + if err := os.Remove(outPath); err == nil { + changed++ + } else if !errors.Is(err, os.ErrNotExist) { + log.Fatalf("remove %s: %v", outPath, err) + } + continue + } + + src, err := generateResetFile(p, targets) + if err != nil { + log.Fatalf("generate for %s: %v", p.PkgPath, err) + } + + cleanPath := filepath.Clean(outPath) + old, err := os.ReadFile(cleanPath) + if err != nil { + log.Fatalf("read %s: %v", outPath, err) + } + if bytes.Equal(old, src) { + continue + } + if err := os.WriteFile(cleanPath, src, 0o600); err != nil { + log.Fatalf("write %s: %v", outPath, err) + } + changed++ + } + + log.Printf("reset: done (changed %d file(s))", changed) +} + +// findModuleRoot finds the root directory of the Go module by looking for go.mod file upwards from the current working directory. +func findModuleRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + dir := wd + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "", fmt.Errorf("go.mod not found вверх по дереву от %s", wd) +} + +type targetStruct struct { + Name string + Named *types.Named +} + +func findTargetStructs(pkg *packages.Package) []targetStruct { + var out []targetStruct + + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + continue + } + + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + _, ok = ts.Type.(*ast.StructType) + if !ok { + continue + } + + if !hasMarker(gd.Doc, ts.Doc) { + continue + } + + obj := pkg.Types.Scope().Lookup(ts.Name.Name) + tn, ok := obj.(*types.TypeName) + if !ok { + continue + } + named, ok := tn.Type().(*types.Named) + if !ok { + continue + } + + if hasResetMethod(named) || hasResetMethod(types.NewPointer(named)) { + log.Printf("reset: skip %s.%s (Reset already exists)", pkg.PkgPath, ts.Name.Name) + continue + } + + out = append(out, targetStruct{ + Name: ts.Name.Name, + Named: named, + }) + } + } + } + + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + +func hasMarker(genDoc, specDoc *ast.CommentGroup) bool { + check := func(cg *ast.CommentGroup) bool { + if cg == nil { + return false + } + for _, c := range cg.List { + txt := c.Text + txt = strings.TrimSpace(txt) + txt = strings.TrimPrefix(txt, "//") + txt = strings.TrimPrefix(txt, "/*") + txt = strings.TrimSuffix(txt, "*/") + txt = strings.TrimSpace(txt) + + txt = strings.TrimSuffix(txt, ";") + if strings.TrimSpace(txt) == marker { + return true + } + } + return false + } + + return check(specDoc) || check(genDoc) +} + +func generateResetFile(pkg *packages.Package, targets []targetStruct) ([]byte, error) { + im := newImportManager(pkg.PkgPath) + + var w codeWriter + w.line(0, "// Code generated by cmd/reset; DO NOT EDIT.") + w.line(0, "") + w.line(0, "package "+pkg.Name) + w.line(0, "") + + var methods bytes.Buffer + mw := codeWriter{buf: &methods} + + for _, t := range targets { + if err := emitResetMethod(&mw, im, pkg, t); err != nil { + return nil, err + } + mw.line(0, "") + } + + imports := im.sortedImports() + if len(imports) > 0 { + w.line(0, "import (") + for _, imp := range imports { + if imp.Alias == "" { + w.line(1, fmt.Sprintf("%q", imp.Path)) + } else { + w.line(1, fmt.Sprintf("%s %q", imp.Alias, imp.Path)) + } + } + w.line(0, ")") + w.line(0, "") + } + + w.buf.Write(methods.Bytes()) + + formatted, err := format.Source(w.buf.Bytes()) + if err != nil { + return nil, fmt.Errorf("format: %w\n\n%s", err, w.buf.String()) + } + return formatted, nil +} + +func emitResetMethod(w *codeWriter, im *importManager, pkg *packages.Package, t targetStruct) error { + st, ok := t.Named.Underlying().(*types.Struct) + if !ok { + return fmt.Errorf("%s is not a struct type", t.Name) + } + + recv := "x" + w.line(0, fmt.Sprintf("func (%s *%s) Reset() {", recv, t.Name)) + w.line(1, fmt.Sprintf("if %s == nil {", recv)) + w.line(2, "return") + w.line(1, "}") + w.line(0, "") + + for i := 0; i < st.NumFields(); i++ { + f := st.Field(i) + fieldExpr := recv + "." + f.Name() + emitResetForExpr(w, im, pkg.PkgPath, fieldExpr, f.Type()) + } + + w.line(0, "}") + return nil +} + +type codeWriter struct { + buf *bytes.Buffer +} + +func (w *codeWriter) ensure() { + if w.buf == nil { + w.buf = &bytes.Buffer{} + } +} + +func (w *codeWriter) line(indent int, s string) { + w.ensure() + for i := 0; i < indent; i++ { + w.buf.WriteByte('\t') + } + w.buf.WriteString(s) + w.buf.WriteByte('\n') +} + +type importSpec struct { + Path string + Alias string +} + +type importManager struct { + localPkgPath string + byPath map[string]string + usedAlias map[string]bool +} + +func newImportManager(localPkgPath string) *importManager { + return &importManager{ + localPkgPath: localPkgPath, + byPath: map[string]string{}, + usedAlias: map[string]bool{}, + } +} + +func (im *importManager) qualifier(p *types.Package) string { + if p == nil { + return "" + } + if p.Path() == im.localPkgPath { + return "" + } + if alias, ok := im.byPath[p.Path()]; ok { + return alias + } + + base := p.Name() + alias := base + if im.usedAlias[alias] { + for i := 2; ; i++ { + alias = fmt.Sprintf("%s%d", base, i) + if !im.usedAlias[alias] { + break + } + } + } + im.usedAlias[alias] = true + im.byPath[p.Path()] = alias + return alias +} + +func (im *importManager) typeString(t types.Type) string { + return types.TypeString(t, im.qualifier) +} + +func (im *importManager) sortedImports() []importSpec { + paths := make([]string, 0, len(im.byPath)) + for p := range im.byPath { + paths = append(paths, p) + } + sort.Strings(paths) + + out := make([]importSpec, 0, len(paths)) + for _, p := range paths { + alias := im.byPath[p] + + last := p + if idx := strings.LastIndex(p, "/"); idx >= 0 { + last = p[idx+1:] + } + if alias == last { + alias = "" + } + + out = append(out, importSpec{Path: p, Alias: alias}) + } + return out +} + +func emitResetForExpr(w *codeWriter, im *importManager, localPkgPath, expr string, t types.Type) { + u := t.Underlying() + + switch tt := u.(type) { + case *types.Basic: + w.line(1, fmt.Sprintf("%s = %s", expr, zeroBasic(tt))) + return + + case *types.Slice: + w.line(1, fmt.Sprintf("%s = %s[:0]", expr, expr)) + return + + case *types.Map: + w.line(1, fmt.Sprintf("clear(%s)", expr)) + return + + case *types.Pointer: + ptrExpr := expr + w.line(1, fmt.Sprintf("if %s != nil {", ptrExpr)) + emitResetThroughPointer(w, im, localPkgPath, ptrExpr, tt.Elem()) + w.line(1, "}") + return + + case *types.Struct: + if hasResetMethod(types.NewPointer(t)) { + w.line(1, fmt.Sprintf("(&%s).Reset()", expr)) + return + } + if hasResetMethod(t) { + w.line(1, fmt.Sprintf("%s.Reset()", expr)) + return + } + + if st, ok := accessibleStruct(t, localPkgPath); ok { + for i := 0; i < st.NumFields(); i++ { + f := st.Field(i) + emitResetForExpr(w, im, localPkgPath, expr+"."+f.Name(), f.Type()) + } + return + } + + w.line(1, fmt.Sprintf("%s = %s{}", expr, im.typeString(t))) + return + + case *types.Array: + w.line(1, fmt.Sprintf("%s = %s{}", expr, im.typeString(t))) + return + + default: + w.line(1, fmt.Sprintf("%s = %s", expr, zeroExpr(im, t))) + return + } +} + +func emitResetThroughPointer(w *codeWriter, im *importManager, localPkgPath, ptrExpr string, elem types.Type) { + u := elem.Underlying() + + switch tt := u.(type) { + case *types.Basic: + w.line(2, fmt.Sprintf("*%s = %s", ptrExpr, zeroBasic(tt))) + return + + case *types.Slice: + w.line(2, fmt.Sprintf("*%s = (*%s)[:0]", ptrExpr, ptrExpr)) + return + + case *types.Map: + w.line(2, fmt.Sprintf("clear(*%s)", ptrExpr)) + return + + case *types.Pointer: + w.line(2, fmt.Sprintf("if *%s != nil {", ptrExpr)) + emitResetThroughPointer(w, im, localPkgPath, "*"+ptrExpr, tt.Elem()) + w.line(2, "}") + return + + case *types.Struct: + if hasResetMethod(types.NewPointer(elem)) { + w.line(2, fmt.Sprintf("%s.Reset()", ptrExpr)) + return + } + + if st, ok := accessibleStruct(elem, localPkgPath); ok { + for i := 0; i < st.NumFields(); i++ { + f := st.Field(i) + emitResetForExpr(w, im, localPkgPath, ptrExpr+"."+f.Name(), f.Type()) + } + return + } + + w.line(2, fmt.Sprintf("*%s = %s{}", ptrExpr, im.typeString(elem))) + return + + case *types.Array: + w.line(2, fmt.Sprintf("*%s = %s{}", ptrExpr, im.typeString(elem))) + return + + default: + w.line(2, fmt.Sprintf("*%s = %s", ptrExpr, zeroExpr(im, elem))) + return + } +} + +func accessibleStruct(t types.Type, localPkgPath string) (*types.Struct, bool) { + switch tt := t.(type) { + case *types.Struct: + return tt, true + case *types.Named: + st, ok := tt.Underlying().(*types.Struct) + if !ok { + return nil, false + } + if tt.Obj() != nil && tt.Obj().Pkg() != nil && tt.Obj().Pkg().Path() == localPkgPath { + return st, true + } + return nil, false + default: + return nil, false + } +} + +func hasResetMethod(t types.Type) bool { + ms := types.NewMethodSet(t) + for i := 0; i < ms.Len(); i++ { + sel := ms.At(i) + if sel.Obj().Name() != "Reset" { + continue + } + fn, ok := sel.Obj().(*types.Func) + if !ok { + continue + } + sig, ok := fn.Type().(*types.Signature) + if !ok { + continue + } + if sig.Params().Len() == 0 && sig.Results().Len() == 0 { + return true + } + } + return false +} + +func zeroBasic(b *types.Basic) string { + switch b.Kind() { + case types.Bool: + return "false" + case types.String: + return `""` + case types.UntypedNil: + const nilString = "nil" + return nilString + default: + return "0" + } +} + +func zeroExpr(im *importManager, t types.Type) string { + u := t.Underlying() + + switch u := u.(type) { + case *types.Basic: + return zeroBasic(u) + case *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface, *types.Pointer: + return "nil" + case *types.Struct, *types.Array: + return im.typeString(t) + "{}" + default: + return "nil" + } +} From 48eedd8e80495637ced1f69989de43b442af28c2 Mon Sep 17 00:00:00 2001 From: vshulcz Date: Sun, 14 Dec 2025 23:47:34 +0300 Subject: [PATCH 3/6] feat: reset pool --- internal/misc/pool.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 internal/misc/pool.go diff --git a/internal/misc/pool.go b/internal/misc/pool.go new file mode 100644 index 0000000..81ab788 --- /dev/null +++ b/internal/misc/pool.go @@ -0,0 +1,37 @@ +package misc + +import "sync" + +// Resetter is an interface for types that can reset their state. +type Resetter interface { + Reset() +} + +// Pool is a generic object pool for types that implement the Resetter interface. +type Pool[T Resetter] struct { + p sync.Pool +} + +// NewPool creates a new Pool for the specified type T. +func NewPool[T Resetter](newFn func() T) *Pool[T] { + pl := &Pool[T]{} + pl.p.New = func() any { + if newFn != nil { + return newFn() + } + var zero T + return zero + } + return pl +} + +// Get retrieves an object from the pool. +func (pl *Pool[T]) Get() T { + return pl.p.Get().(T) +} + +// Put returns an object to the pool after resetting it. +func (pl *Pool[T]) Put(v T) { + v.Reset() + pl.p.Put(v) +} From 20a4680e22df56ffa8795d4055add4053d23bcae Mon Sep 17 00:00:00 2001 From: vshulcz Date: Mon, 15 Dec 2025 00:00:05 +0300 Subject: [PATCH 4/6] feat: version linker flags --- README.md | 21 ++++++++++++++++++++- cmd/agent/main.go | 22 ++++++++++++++++++++++ cmd/server/main.go | 22 ++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a1ee5d..c558ba8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,25 @@ go run ./cmd/agent -a http://localhost:8080 -r 10 -p 2 -l 2 # -r report interval (s), -p poll interval (s), -l parallel senders ``` +3) Build with version info +To include build version, date, and commit in the binaries, use the following commands: + +```bash +go build -o bin/server \ + -ldflags "\ + -X 'main.buildVersion=v1.2.5' \ + -X 'main.buildDate=2025-12-14T10:00:00Z' \ + -X 'main.buildCommit=abc1234' \ + " ./cmd/server + +go build -o bin/agent \ + -ldflags "\ + -X 'main.buildVersion=v1.2.5' \ + -X 'main.buildDate=2025-12-14T10:00:00Z' \ + -X 'main.buildCommit=abc1234' \ + " ./cmd/agent +``` + Open http://localhost:8080 to see metrics. ## API (HTTP) @@ -126,4 +145,4 @@ You can use ENV, CLI flags, or defaults (ENV > CLI > defaults). ## Metrics you’ll see -Go runtime gauges like Alloc, HeapAlloc, NumGC, PauseTotalNs, plus host gauges TotalMemory, FreeMemory, and per-core CPUutilization{N}; counters include PollCount, etc. \ No newline at end of file +Go runtime gauges like Alloc, HeapAlloc, NumGC, PauseTotalNs, plus host gauges TotalMemory, FreeMemory, and per-core CPUutilization{N}; counters include PollCount, etc. diff --git a/cmd/agent/main.go b/cmd/agent/main.go index a42fd32..a4d889e 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "log" "net/http" "os" @@ -14,7 +15,15 @@ import ( agentsvc "github.com/vshulcz/Golectra/internal/services/agent" ) +var ( + buildVersion string + buildDate string + buildCommit string +) + func main() { + printBuildInfo() + cfg, err := config.LoadAgentConfig(os.Args[1:], nil) if err != nil { log.Fatalf("failed to parse flags: %v", err) @@ -36,3 +45,16 @@ func main() { log.Fatal(err) } } + +func na(v string) string { + if v == "" { + return "N/A" + } + return v +} + +func printBuildInfo() { + fmt.Printf("Build version: %s\n", na(buildVersion)) + fmt.Printf("Build date: %s\n", na(buildDate)) + fmt.Printf("Build commit: %s\n", na(buildCommit)) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 1d40f04..d690dc0 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "log" "net/http" "os" @@ -19,7 +20,15 @@ import ( "go.uber.org/zap" ) +var ( + buildVersion string + buildDate string + buildCommit string +) + func main() { + printBuildInfo() + if err := run(os.Args[1:]); err != nil { log.Fatal(err) } @@ -115,3 +124,16 @@ func buildAuditor(cfg config.ServerConfig, logger *zap.Logger) audit.Publisher { } return subject } + +func na(v string) string { + if v == "" { + return "N/A" + } + return v +} + +func printBuildInfo() { + fmt.Printf("Build version: %s\n", na(buildVersion)) + fmt.Printf("Build date: %s\n", na(buildDate)) + fmt.Printf("Build commit: %s\n", na(buildCommit)) +} From 96a2e6b460db39239f4b0d8e0862dd57aab8ad9b Mon Sep 17 00:00:00 2001 From: vshulcz Date: Mon, 15 Dec 2025 00:18:37 +0300 Subject: [PATCH 5/6] feat: test coverage increased --- cmd/agent/main_test.go | 11 ++ cmd/reset/main.go | 4 +- cmd/reset/main_test.go | 155 ++++++++++++++++++ cmd/server/main_test.go | 11 ++ cmd/staticlint/main_test.go | 62 +++++++ cmd/staticlint/osexitmain/osexitmain.go | 16 +- cmd/staticlint/osexitmain/osexitmain_test.go | 134 +++++++++++++++ .../adapters/http/ginserver/handler_test.go | 139 ++++++++++++++++ .../publisher/httpjson/client_test.go | 95 +++++++++++ internal/misc/pool_test.go | 70 ++++++++ 10 files changed, 688 insertions(+), 9 deletions(-) create mode 100644 cmd/agent/main_test.go create mode 100644 cmd/reset/main_test.go create mode 100644 cmd/server/main_test.go create mode 100644 cmd/staticlint/main_test.go create mode 100644 cmd/staticlint/osexitmain/osexitmain_test.go create mode 100644 internal/misc/pool_test.go diff --git a/cmd/agent/main_test.go b/cmd/agent/main_test.go new file mode 100644 index 0000000..66af1ec --- /dev/null +++ b/cmd/agent/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" +) + +func TestBuildVariablesExist(t *testing.T) { + _ = buildVersion + _ = buildDate + _ = buildCommit +} diff --git a/cmd/reset/main.go b/cmd/reset/main.go index c8f8200..067ba67 100644 --- a/cmd/reset/main.go +++ b/cmd/reset/main.go @@ -179,13 +179,13 @@ func hasMarker(genDoc, specDoc *ast.CommentGroup) bool { for _, c := range cg.List { txt := c.Text txt = strings.TrimSpace(txt) + txt = strings.ToLower(txt) txt = strings.TrimPrefix(txt, "//") txt = strings.TrimPrefix(txt, "/*") txt = strings.TrimSuffix(txt, "*/") txt = strings.TrimSpace(txt) - txt = strings.TrimSuffix(txt, ";") - if strings.TrimSpace(txt) == marker { + if txt == "+"+marker { return true } } diff --git a/cmd/reset/main_test.go b/cmd/reset/main_test.go new file mode 100644 index 0000000..855c38c --- /dev/null +++ b/cmd/reset/main_test.go @@ -0,0 +1,155 @@ +package main + +import ( + "bytes" + "go/ast" + "go/types" + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/go/packages" +) + +func TestFindModuleRoot(t *testing.T) { + root, err := findModuleRoot() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if _, err := os.Stat(filepath.Join(root, "go.mod")); os.IsNotExist(err) { + t.Errorf("expected go.mod file in module root, but not found") + } +} + +func TestGenerateResetFile(t *testing.T) { + pkg := &packages.Package{ + Name: "testpkg", + PkgPath: "github.com/example/testpkg", + } + + targets := []targetStruct{ + { + Name: "TestStruct", + Named: types.NewNamed( + types.NewTypeName(0, nil, "TestStruct", nil), + types.NewStruct(nil, nil), + nil, + ), + }, + } + + result, err := generateResetFile(pkg, targets) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !bytes.Contains(result, []byte("package testpkg")) { + t.Errorf("expected package declaration in generated file") + } +} + +func TestHasMarker(t *testing.T) { + comment := &ast.CommentGroup{ + List: []*ast.Comment{ + {Text: "// +generate:reset"}, + }, + } + + if !hasMarker(comment, nil) { + t.Errorf("expected marker to be detected") + } +} + +func TestHasMarkerNoMarker(t *testing.T) { + comment := &ast.CommentGroup{ + List: []*ast.Comment{ + {Text: "// regular comment"}, + }, + } + + if hasMarker(comment, nil) { + t.Errorf("expected no marker to be detected") + } +} + +func TestHasMarkerBothDocs(t *testing.T) { + genDoc := &ast.CommentGroup{ + List: []*ast.Comment{ + {Text: "// +generate:reset"}, + }, + } + + if !hasMarker(genDoc, nil) { + t.Errorf("expected marker in genDoc to be detected") + } +} + +func TestZeroBasic(t *testing.T) { + tests := []struct { + name string + kind types.BasicKind + expected string + }{ + { + name: "bool zero", + kind: types.Bool, + expected: "false", + }, + { + name: "string zero", + kind: types.String, + expected: `""`, + }, + { + name: "int zero", + kind: types.Int, + expected: "0", + }, + { + name: "float64 zero", + kind: types.Float64, + expected: "0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + basic := types.Typ[tt.kind] + result := zeroBasic(basic) + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + +func TestZeroExpr(t *testing.T) { + im := newImportManager("test") + + tests := []struct { + name string + typ types.Type + expected string + }{ + { + name: "basic nil", + typ: types.Typ[types.UntypedNil], + expected: "nil", + }, + { + name: "slice", + typ: types.NewSlice(types.Typ[types.Int]), + expected: "nil", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := zeroExpr(im, tt.typ) + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go new file mode 100644 index 0000000..66af1ec --- /dev/null +++ b/cmd/server/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" +) + +func TestBuildVariablesExist(t *testing.T) { + _ = buildVersion + _ = buildDate + _ = buildCommit +} diff --git a/cmd/staticlint/main_test.go b/cmd/staticlint/main_test.go new file mode 100644 index 0000000..7fa3e37 --- /dev/null +++ b/cmd/staticlint/main_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "testing" + + "golang.org/x/tools/go/analysis" +) + +func TestFilterAnalyzers(t *testing.T) { + tests := []struct { + name string + input []*analysis.Analyzer + expected int + }{ + { + name: "filter Golectra analyzers", + input: []*analysis.Analyzer{ + {Name: "GolectraAnalyzer1"}, + {Name: "GolectraAnalyzer2"}, + {Name: "OtherAnalyzer"}, + }, + expected: 2, + }, + { + name: "no Golectra analyzers", + input: []*analysis.Analyzer{ + {Name: "OtherAnalyzer1"}, + {Name: "OtherAnalyzer2"}, + }, + expected: 0, + }, + { + name: "empty input", + input: []*analysis.Analyzer{}, + expected: 0, + }, + { + name: "all Golectra analyzers", + input: []*analysis.Analyzer{ + {Name: "GolectraAnalyzer1"}, + {Name: "GolectraAnalyzer2"}, + }, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filtered := filterAnalyzers(tt.input) + + if len(filtered) != tt.expected { + t.Errorf("expected %d analyzers, got %d", tt.expected, len(filtered)) + } + + for _, a := range filtered { + if a.Name != "GolectraAnalyzer1" && a.Name != "GolectraAnalyzer2" { + t.Errorf("unexpected analyzer: %s", a.Name) + } + } + }) + } +} diff --git a/cmd/staticlint/osexitmain/osexitmain.go b/cmd/staticlint/osexitmain/osexitmain.go index 975aaca..f2caf5e 100644 --- a/cmd/staticlint/osexitmain/osexitmain.go +++ b/cmd/staticlint/osexitmain/osexitmain.go @@ -32,10 +32,7 @@ func run(pass *analysis.Pass) (any, error) { insp.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) { fd, ok := n.(*ast.FuncDecl) - if !ok { - return - } - if fd.Recv != nil || fd.Name == nil || fd.Name.Name != "main" || fd.Body == nil { + if !ok || fd.Recv != nil || fd.Name == nil || fd.Name.Name != "main" || fd.Body == nil { return } @@ -45,7 +42,7 @@ func run(pass *analysis.Pass) (any, error) { return false case *ast.CallExpr: if isOsExitCall(pass, x) { - pass.Reportf(x.Lparen, "It is forbidden to call os.Exit directly in main function; use return code from main instead") + pass.Reportf(x.Pos(), "It is forbidden to call os.Exit directly in main function; use return code from main instead") } } return true @@ -57,14 +54,19 @@ func run(pass *analysis.Pass) (any, error) { // isOsExitCall checks if the given call expression is a call to os.Exit. func isOsExitCall(pass *analysis.Pass, call *ast.CallExpr) bool { + if call == nil || call.Fun == nil { + return false + } + sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok || sel.Sel == nil { + if !ok || sel.Sel == nil || sel.X == nil { return false } - if pass.TypesInfo == nil { + if pass.TypesInfo == nil || pass.TypesInfo.Uses == nil { return false } + obj := pass.TypesInfo.Uses[sel.Sel] fn, ok := obj.(*types.Func) if !ok || fn.Pkg() == nil { diff --git a/cmd/staticlint/osexitmain/osexitmain_test.go b/cmd/staticlint/osexitmain/osexitmain_test.go new file mode 100644 index 0000000..346f58d --- /dev/null +++ b/cmd/staticlint/osexitmain/osexitmain_test.go @@ -0,0 +1,134 @@ +package osexitmain + +import ( + "go/ast" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" +) + +func mockPass(pkgName string, nodes []ast.Node) *analysis.Pass { + return &analysis.Pass{ + Pkg: types.NewPackage(pkgName, ""), + ResultOf: map[*analysis.Analyzer]any{ + inspect.Analyzer: &mockInspector{nodes: nodes}, + }, + } +} + +type mockInspector struct { + nodes []ast.Node +} + +func (m *mockInspector) Preorder(_ []ast.Node, fn func(ast.Node)) { + for _, n := range m.nodes { + fn(n) + } +} + +func TestRun(t *testing.T) { + tests := []struct { + name string + pkgName string + nodes []ast.Node + }{ + { + name: "no os.Exit call", + pkgName: "main", + nodes: []ast.Node{ + &ast.FuncDecl{ + Name: &ast.Ident{Name: "main"}, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{ + X: &ast.BasicLit{Kind: token.STRING, Value: "\"hello\""}, + }, + }, + }, + }, + }, + }, + { + name: "os.Exit call in main", + pkgName: "main", + nodes: []ast.Node{ + &ast.FuncDecl{ + Name: &ast.Ident{Name: "main"}, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "os"}, + Sel: &ast.Ident{Name: "Exit"}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pass := mockPass(tt.pkgName, tt.nodes) + _, err := run(pass) + if err != nil { + t.Errorf("run() returned unexpected error: %v", err) + } + }) + } +} + +func TestIsOsExitCall(t *testing.T) { + tests := []struct { + name string + callExpr *ast.CallExpr + expectRes bool + }{ + { + name: "valid os.Exit call", + callExpr: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "os"}, + Sel: &ast.Ident{Name: "Exit"}, + }, + }, + expectRes: true, + }, + { + name: "non os.Exit call", + callExpr: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "fmt"}, + Sel: &ast.Ident{Name: "Println"}, + }, + }, + expectRes: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pass := &analysis.Pass{ + TypesInfo: &types.Info{ + Uses: map[*ast.Ident]types.Object{ + tt.callExpr.Fun.(*ast.SelectorExpr).Sel: types.NewFunc(0, types.NewPackage("os", "os"), "Exit", types.NewSignatureType(nil, nil, nil, nil, nil, false)), + }, + }, + } + if tt.name == "non os.Exit call" { + pass.TypesInfo.Uses[tt.callExpr.Fun.(*ast.SelectorExpr).Sel] = types.NewFunc(0, types.NewPackage("fmt", "fmt"), "Println", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + } + res := isOsExitCall(pass, tt.callExpr) + if res != tt.expectRes { + t.Errorf("isOsExitCall() = %v, expectRes %v", res, tt.expectRes) + } + }) + } +} diff --git a/internal/adapters/http/ginserver/handler_test.go b/internal/adapters/http/ginserver/handler_test.go index fb9e73b..97a8a2d 100644 --- a/internal/adapters/http/ginserver/handler_test.go +++ b/internal/adapters/http/ginserver/handler_test.go @@ -681,3 +681,142 @@ func TestSnapshotJSON_OK(t *testing.T) { t.Fatalf("counters.PollCount=%v", got.Counters["PollCount"]) } } + +func TestDecodeMetricsBatch(t *testing.T) { + tests := []struct { + name string + input []domain.Metrics + expectErr bool + }{ + { + name: "valid batch", + input: []domain.Metrics{ + {ID: "Alloc", MType: string(domain.Gauge), Value: ptrFloat(100)}, + {ID: "Counter1", MType: string(domain.Counter), Delta: ptrInt64(5)}, + }, + expectErr: false, + }, + { + name: "empty batch", + input: []domain.Metrics{}, + expectErr: false, + }, + { + name: "single metric", + input: []domain.Metrics{ + {ID: "TestMetric", MType: string(domain.Gauge), Value: ptrFloat(42.5)}, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encoded, err := json.Marshal(tt.input) + if err != nil { + t.Fatalf("failed to encode: %v", err) + } + + reader := bytes.NewReader(encoded) + items, cleanup, err := decodeMetricsBatch(reader) + defer cleanup() + + if (err != nil) != tt.expectErr { + t.Errorf("expected error: %v, got: %v", tt.expectErr, err) + } + + if !tt.expectErr && len(items) != len(tt.input) { + t.Errorf("expected %d items, got %d", len(tt.input), len(items)) + } + }) + } +} + +func TestDecodeMetricsBatchInvalid(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "invalid JSON", + input: "{ invalid json", + }, + { + name: "wrong type", + input: `"not an array"`, + }, + { + name: "empty input", + input: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytes.NewReader([]byte(tt.input)) + _, cleanup, err := decodeMetricsBatch(reader) + defer cleanup() + + if err == nil { + t.Errorf("expected error for input: %s", tt.name) + } + }) + } +} + +func TestCloneMetrics(t *testing.T) { + tests := []struct { + name string + input []domain.Metrics + expected int + }{ + { + name: "clone non-empty slice", + input: []domain.Metrics{ + {ID: "Metric1", MType: string(domain.Gauge)}, + {ID: "Metric2", MType: string(domain.Counter)}, + }, + expected: 2, + }, + { + name: "clone empty slice", + input: []domain.Metrics{}, + expected: 0, + }, + { + name: "clone nil slice", + input: nil, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cloned := cloneMetrics(tt.input) + + if tt.expected == 0 && cloned != nil { + t.Errorf("expected nil for empty input, got non-nil") + } + + if tt.expected > 0 && len(cloned) != tt.expected { + t.Errorf("expected %d items, got %d", tt.expected, len(cloned)) + } + + if len(tt.input) > 0 && len(cloned) > 0 { + originalID := tt.input[0].ID + cloned[0].ID = "modified" + if tt.input[0].ID != originalID { + t.Error("modification of clone should not affect original") + } + } + }) + } +} + +func ptrFloat(f float64) *float64 { + return &f +} + +func ptrInt64(i int64) *int64 { + return &i +} diff --git a/internal/adapters/publisher/httpjson/client_test.go b/internal/adapters/publisher/httpjson/client_test.go index a3ad1bd..54d649c 100644 --- a/internal/adapters/publisher/httpjson/client_test.go +++ b/internal/adapters/publisher/httpjson/client_test.go @@ -1,6 +1,7 @@ package httpjson import ( + "bytes" "compress/gzip" "context" "encoding/json" @@ -864,3 +865,97 @@ type nopWriteCloser struct{ *strings.Builder } func (n *nopWriteCloser) Write(p []byte) (int, error) { return n.Builder.Write(p) } func (*nopWriteCloser) Close() error { return nil } + +func TestGzipBytes(t *testing.T) { + tests := []struct { + name string + input []byte + wantErr bool + }{ + { + name: "valid input", + input: []byte("test data"), + wantErr: false, + }, + { + name: "empty input", + input: []byte{}, + wantErr: false, + }, + { + name: "large input", + input: bytes.Repeat([]byte("x"), 10000), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payload, err := gzipBytes(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("gzipBytes() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err == nil && payload == nil { + t.Error("expected non-nil payload") + return + } + + if err == nil { + compressed := payload.Bytes() + reader, err := gzip.NewReader(bytes.NewReader(compressed)) + if err != nil { + t.Errorf("failed to create gzip reader: %v", err) + return + } + defer func() { + _ = reader.Close() + }() + + decompressed := new(bytes.Buffer) + if _, err := decompressed.ReadFrom(reader); err != nil { + t.Errorf("failed to decompress: %v", err) + return + } + + if !bytes.Equal(decompressed.Bytes(), tt.input) { + t.Errorf("decompressed data does not match original input") + } + + payload.Release() + } + }) + } +} + +func TestCompressedPayload(t *testing.T) { + t.Run("bytes returns nil for nil payload", func(t *testing.T) { + var p *compressedPayload + if p.Bytes() != nil { + t.Error("expected nil for nil payload") + } + }) + + t.Run("release handles nil payload gracefully", func(t *testing.T) { + var p *compressedPayload + p.Release() // Should not panic + }) + + t.Run("bytes returns data after gzip", func(t *testing.T) { + payload, err := gzipBytes([]byte("test")) + if err != nil { + t.Fatalf("gzipBytes failed: %v", err) + } + defer payload.Release() + + data := payload.Bytes() + if data == nil { + t.Error("expected non-nil data") + } + + if len(data) == 0 { + t.Error("expected non-empty data") + } + }) +} diff --git a/internal/misc/pool_test.go b/internal/misc/pool_test.go new file mode 100644 index 0000000..1a83bb0 --- /dev/null +++ b/internal/misc/pool_test.go @@ -0,0 +1,70 @@ +package misc + +import ( + "sync" + "testing" +) + +type mockResetter struct { + resetCalled bool +} + +func (m *mockResetter) Reset() { + m.resetCalled = true +} + +func TestNewPool(t *testing.T) { + pool := NewPool(func() *mockResetter { + return &mockResetter{} + }) + + if pool == nil { + t.Fatal("expected pool to be created, got nil") + } +} + +func TestPoolGet(t *testing.T) { + pool := NewPool(func() *mockResetter { + return &mockResetter{} + }) + + item := pool.Get() + if item == nil { + t.Fatal("expected item to be non-nil, got nil") + } +} + +func TestPoolPut(t *testing.T) { + pool := NewPool(func() *mockResetter { + return &mockResetter{} + }) + + // Put an item into the pool - Reset should be called on it + item := &mockResetter{resetCalled: false} + pool.Put(item) + + // After Put, the item should have Reset called + if !item.resetCalled { + t.Fatal("expected Reset to be called on item when Put, but it wasn't") + } +} + +func TestPoolConcurrency(t *testing.T) { + pool := NewPool(func() *mockResetter { + return &mockResetter{} + }) + + var wg sync.WaitGroup + const numGoroutines = 100 + + wg.Add(numGoroutines) + for range numGoroutines { + go func() { + defer wg.Done() + item := pool.Get() + pool.Put(item) + }() + } + + wg.Wait() +} From 596ee56de783d6008902cc3c85a7968426e7ef7f Mon Sep 17 00:00:00 2001 From: vshulcz Date: Tue, 16 Dec 2025 00:55:27 +0300 Subject: [PATCH 6/6] fix: review improvements --- cmd/agent/main.go | 13 +- cmd/reset/args.go | 28 ++ cmd/reset/helpers.go | 167 +++++++++ cmd/reset/logic.go | 180 ++++++++++ cmd/reset/main.go | 344 +------------------ cmd/server/main.go | 13 +- cmd/staticlint/main.go | 15 +- cmd/staticlint/main_test.go | 62 ---- cmd/staticlint/osexitmain/osexitmain.go | 6 + cmd/staticlint/osexitmain/osexitmain_test.go | 16 +- internal/misc/pool.go | 7 +- pkg/util/common.go | 19 + 12 files changed, 424 insertions(+), 446 deletions(-) create mode 100644 cmd/reset/args.go create mode 100644 cmd/reset/helpers.go create mode 100644 cmd/reset/logic.go delete mode 100644 cmd/staticlint/main_test.go create mode 100644 pkg/util/common.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index a4d889e..861c232 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "log" "net/http" "os" @@ -13,6 +12,7 @@ import ( "github.com/vshulcz/Golectra/internal/adapters/publisher/httpjson" "github.com/vshulcz/Golectra/internal/config" agentsvc "github.com/vshulcz/Golectra/internal/services/agent" + "github.com/vshulcz/Golectra/pkg/util" ) var ( @@ -46,15 +46,6 @@ func main() { } } -func na(v string) string { - if v == "" { - return "N/A" - } - return v -} - func printBuildInfo() { - fmt.Printf("Build version: %s\n", na(buildVersion)) - fmt.Printf("Build date: %s\n", na(buildDate)) - fmt.Printf("Build commit: %s\n", na(buildCommit)) + util.PrintBuildInfo(buildVersion, buildDate, buildCommit) } diff --git a/cmd/reset/args.go b/cmd/reset/args.go new file mode 100644 index 0000000..91f5400 --- /dev/null +++ b/cmd/reset/args.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +// findModuleRoot finds the root directory of the Go module by looking for the go.mod file upwards from the current working directory. +func findModuleRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + dir := wd + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "", fmt.Errorf("go.mod not found upwards from %s", wd) +} diff --git a/cmd/reset/helpers.go b/cmd/reset/helpers.go new file mode 100644 index 0000000..a628ea2 --- /dev/null +++ b/cmd/reset/helpers.go @@ -0,0 +1,167 @@ +package main + +import ( + "fmt" + "go/ast" + "go/types" + "sort" + "strings" +) + +// hasMarker checks if the given comment groups contain the marker. +func hasMarker(genDoc, specDoc *ast.CommentGroup) bool { + check := func(cg *ast.CommentGroup) bool { + if cg == nil { + return false + } + for _, c := range cg.List { + txt := c.Text + txt = strings.TrimSpace(txt) + txt = strings.ToLower(txt) + txt = strings.TrimPrefix(txt, "//") + txt = strings.TrimPrefix(txt, "/*") + txt = strings.TrimSuffix(txt, "*/") + txt = strings.TrimSpace(txt) + + if txt == "+"+marker { + return true + } + } + return false + } + + return check(specDoc) || check(genDoc) +} + +// hasResetMethod checks if the given type has a Reset method. +func hasResetMethod(t types.Type) bool { + ms := types.NewMethodSet(t) + for i := 0; i < ms.Len(); i++ { + sel := ms.At(i) + if sel.Obj().Name() != "Reset" { + continue + } + fn, ok := sel.Obj().(*types.Func) + if !ok { + continue + } + sig, ok := fn.Type().(*types.Signature) + if !ok { + continue + } + if sig.Params().Len() == 0 && sig.Results().Len() == 0 { + return true + } + } + return false +} + +// zeroBasic returns the zero value for a basic type. +func zeroBasic(b *types.Basic) string { + switch b.Kind() { + case types.Bool: + return "false" + case types.String: + return `""` + case types.UntypedNil: + const nilString = "nil" + return nilString + default: + return "0" + } +} + +// zeroExpr returns the zero value for a given type. +func zeroExpr(im *importManager, t types.Type) string { + u := t.Underlying() + + switch u := u.(type) { + case *types.Basic: + return zeroBasic(u) + case *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface, *types.Pointer: + return "nil" + case *types.Struct, *types.Array: + return im.typeString(t) + "{}" + default: + return "nil" + } +} + +// importSpec represents an import path and its alias. +type importSpec struct { + Path string + Alias string +} + +// importManager manages imports for generated code. +type importManager struct { + localPkgPath string + byPath map[string]string + usedAlias map[string]bool +} + +// newImportManager creates a new import manager. +func newImportManager(localPkgPath string) *importManager { + return &importManager{ + localPkgPath: localPkgPath, + byPath: map[string]string{}, + usedAlias: map[string]bool{}, + } +} + +// qualifier returns the alias for a package. +func (im *importManager) qualifier(p *types.Package) string { + if p == nil { + return "" + } + if p.Path() == im.localPkgPath { + return "" + } + if alias, ok := im.byPath[p.Path()]; ok { + return alias + } + + base := p.Name() + alias := base + if im.usedAlias[alias] { + for i := 2; ; i++ { + alias = fmt.Sprintf("%s%d", base, i) + if !im.usedAlias[alias] { + break + } + } + } + im.usedAlias[alias] = true + im.byPath[p.Path()] = alias + return alias +} + +// typeString returns the string representation of a type. +func (im *importManager) typeString(t types.Type) string { + return types.TypeString(t, im.qualifier) +} + +// sortedImports returns the sorted list of imports. +func (im *importManager) sortedImports() []importSpec { + paths := make([]string, 0, len(im.byPath)) + for p := range im.byPath { + paths = append(paths, p) + } + sort.Strings(paths) + + out := make([]importSpec, 0, len(paths)) + for _, p := range paths { + alias := im.byPath[p] + + last := p + if idx := strings.LastIndex(p, "/"); idx >= 0 { + last = p[idx+1:] + } + if alias == last { + alias = "" + } + + out = append(out, importSpec{Path: p, Alias: alias}) + } + return out +} diff --git a/cmd/reset/logic.go b/cmd/reset/logic.go new file mode 100644 index 0000000..4d2a68c --- /dev/null +++ b/cmd/reset/logic.go @@ -0,0 +1,180 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "golang.org/x/tools/go/packages" +) + +const marker = "generate:reset" + +type targetStruct struct { + Name string + Named *types.Named +} + +func processPackages(root string) { + cfg := &packages.Config{ + Dir: root, + Mode: packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedSyntax | + packages.NeedTypes | + packages.NeedTypesInfo, + } + + pkgs, err := packages.Load(cfg, "./...") + if err != nil { + log.Fatal(err) + } + + changed := 0 + for _, p := range pkgs { + if len(p.Errors) > 0 { + var b strings.Builder + fmt.Fprintf(&b, "package load errors:\n") + for _, e := range p.Errors { + fmt.Fprintf(&b, " - %s\n", e) + } + log.Fatal(b.String()) + } + + if len(p.CompiledGoFiles) == 0 { + continue + } + + targets := findTargetStructs(p) + outPath := filepath.Join(filepath.Dir(p.CompiledGoFiles[0]), "reset.gen.go") + + if len(targets) == 0 { + if err := os.Remove(outPath); err == nil { + changed++ + } else if !errors.Is(err, os.ErrNotExist) { + log.Fatalf("remove %s: %v", outPath, err) + } + continue + } + + src, err := generateResetFile(p, targets) + if err != nil { + log.Fatalf("generate for %s: %v", p.PkgPath, err) + } + + cleanPath := filepath.Clean(outPath) + old, err := os.ReadFile(cleanPath) + if err == nil && bytes.Equal(old, src) { + continue + } + if err := os.WriteFile(cleanPath, src, 0o600); err != nil { + log.Fatalf("write %s: %v", outPath, err) + } + changed++ + } + + log.Printf("reset: done (changed %d file(s))", changed) +} + +func findTargetStructs(pkg *packages.Package) []targetStruct { + var out []targetStruct + + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + continue + } + + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + _, ok = ts.Type.(*ast.StructType) + if !ok { + continue + } + + if !hasMarker(gd.Doc, ts.Doc) { + continue + } + + obj := pkg.Types.Scope().Lookup(ts.Name.Name) + tn, ok := obj.(*types.TypeName) + if !ok { + continue + } + named, ok := tn.Type().(*types.Named) + if !ok { + continue + } + + if hasResetMethod(named) || hasResetMethod(types.NewPointer(named)) { + log.Printf("reset: skip %s.%s (Reset already exists)", pkg.PkgPath, ts.Name.Name) + continue + } + + out = append(out, targetStruct{ + Name: ts.Name.Name, + Named: named, + }) + } + } + } + + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + +func generateResetFile(pkg *packages.Package, targets []targetStruct) ([]byte, error) { + im := newImportManager(pkg.PkgPath) + + var w codeWriter + w.line(0, "// Code generated by cmd/reset; DO NOT EDIT.") + w.line(0, "") + w.line(0, "package "+pkg.Name) + w.line(0, "") + + var methods bytes.Buffer + mw := codeWriter{buf: &methods} + + for _, t := range targets { + if err := emitResetMethod(&mw, im, pkg, t); err != nil { + return nil, err + } + mw.line(0, "") + } + + imports := im.sortedImports() + if len(imports) > 0 { + w.line(0, "import (") + for _, imp := range imports { + if imp.Alias == "" { + w.line(1, fmt.Sprintf("%q", imp.Path)) + } else { + w.line(1, fmt.Sprintf("%s %q", imp.Alias, imp.Path)) + } + } + w.line(0, ")") + w.line(0, "") + } + + w.buf.Write(methods.Bytes()) + + formatted, err := format.Source(w.buf.Bytes()) + if err != nil { + return nil, fmt.Errorf("format: %w\n\n%s", err, w.buf.String()) + } + return formatted, nil +} diff --git a/cmd/reset/main.go b/cmd/reset/main.go index 067ba67..e38454c 100644 --- a/cmd/reset/main.go +++ b/cmd/reset/main.go @@ -1,240 +1,22 @@ // Command reset generates Reset() methods for structs marked with "// generate:reset" comment. -// It supports basic types, slices, maps, pointers, and nested structs. -// For structs from other packages, it assigns zero values instead of resetting fields. package main import ( "bytes" - "errors" "fmt" - "go/ast" - "go/format" - "go/token" "go/types" "log" - "os" - "path/filepath" - "sort" - "strings" "golang.org/x/tools/go/packages" ) -const marker = "generate:reset" - func main() { root, err := findModuleRoot() if err != nil { log.Fatal(err) } - cfg := &packages.Config{ - Dir: root, - Mode: packages.NeedName | - packages.NeedFiles | - packages.NeedCompiledGoFiles | - packages.NeedSyntax | - packages.NeedTypes | - packages.NeedTypesInfo, - } - - pkgs, err := packages.Load(cfg, "./...") - if err != nil { - log.Fatal(err) - } - - for _, p := range pkgs { - if len(p.Errors) > 0 { - var b strings.Builder - fmt.Fprintf(&b, "package load errors:\n") - for _, e := range p.Errors { - fmt.Fprintf(&b, " - %s\n", e) - } - log.Fatal(b.String()) - } - } - - changed := 0 - for _, p := range pkgs { - if len(p.CompiledGoFiles) == 0 { - continue - } - - targets := findTargetStructs(p) - outPath := filepath.Join(filepath.Dir(p.CompiledGoFiles[0]), "reset.gen.go") - - if len(targets) == 0 { - if err := os.Remove(outPath); err == nil { - changed++ - } else if !errors.Is(err, os.ErrNotExist) { - log.Fatalf("remove %s: %v", outPath, err) - } - continue - } - - src, err := generateResetFile(p, targets) - if err != nil { - log.Fatalf("generate for %s: %v", p.PkgPath, err) - } - - cleanPath := filepath.Clean(outPath) - old, err := os.ReadFile(cleanPath) - if err != nil { - log.Fatalf("read %s: %v", outPath, err) - } - if bytes.Equal(old, src) { - continue - } - if err := os.WriteFile(cleanPath, src, 0o600); err != nil { - log.Fatalf("write %s: %v", outPath, err) - } - changed++ - } - - log.Printf("reset: done (changed %d file(s))", changed) -} - -// findModuleRoot finds the root directory of the Go module by looking for go.mod file upwards from the current working directory. -func findModuleRoot() (string, error) { - wd, err := os.Getwd() - if err != nil { - return "", err - } - dir := wd - - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - return dir, nil - } - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - return "", fmt.Errorf("go.mod not found вверх по дереву от %s", wd) -} - -type targetStruct struct { - Name string - Named *types.Named -} - -func findTargetStructs(pkg *packages.Package) []targetStruct { - var out []targetStruct - - for _, f := range pkg.Syntax { - for _, decl := range f.Decls { - gd, ok := decl.(*ast.GenDecl) - if !ok || gd.Tok != token.TYPE { - continue - } - - for _, spec := range gd.Specs { - ts, ok := spec.(*ast.TypeSpec) - if !ok { - continue - } - _, ok = ts.Type.(*ast.StructType) - if !ok { - continue - } - - if !hasMarker(gd.Doc, ts.Doc) { - continue - } - - obj := pkg.Types.Scope().Lookup(ts.Name.Name) - tn, ok := obj.(*types.TypeName) - if !ok { - continue - } - named, ok := tn.Type().(*types.Named) - if !ok { - continue - } - - if hasResetMethod(named) || hasResetMethod(types.NewPointer(named)) { - log.Printf("reset: skip %s.%s (Reset already exists)", pkg.PkgPath, ts.Name.Name) - continue - } - - out = append(out, targetStruct{ - Name: ts.Name.Name, - Named: named, - }) - } - } - } - - sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) - return out -} - -func hasMarker(genDoc, specDoc *ast.CommentGroup) bool { - check := func(cg *ast.CommentGroup) bool { - if cg == nil { - return false - } - for _, c := range cg.List { - txt := c.Text - txt = strings.TrimSpace(txt) - txt = strings.ToLower(txt) - txt = strings.TrimPrefix(txt, "//") - txt = strings.TrimPrefix(txt, "/*") - txt = strings.TrimSuffix(txt, "*/") - txt = strings.TrimSpace(txt) - - if txt == "+"+marker { - return true - } - } - return false - } - - return check(specDoc) || check(genDoc) -} - -func generateResetFile(pkg *packages.Package, targets []targetStruct) ([]byte, error) { - im := newImportManager(pkg.PkgPath) - - var w codeWriter - w.line(0, "// Code generated by cmd/reset; DO NOT EDIT.") - w.line(0, "") - w.line(0, "package "+pkg.Name) - w.line(0, "") - - var methods bytes.Buffer - mw := codeWriter{buf: &methods} - - for _, t := range targets { - if err := emitResetMethod(&mw, im, pkg, t); err != nil { - return nil, err - } - mw.line(0, "") - } - - imports := im.sortedImports() - if len(imports) > 0 { - w.line(0, "import (") - for _, imp := range imports { - if imp.Alias == "" { - w.line(1, fmt.Sprintf("%q", imp.Path)) - } else { - w.line(1, fmt.Sprintf("%s %q", imp.Alias, imp.Path)) - } - } - w.line(0, ")") - w.line(0, "") - } - - w.buf.Write(methods.Bytes()) - - formatted, err := format.Source(w.buf.Bytes()) - if err != nil { - return nil, fmt.Errorf("format: %w\n\n%s", err, w.buf.String()) - } - return formatted, nil + processPackages(root) } func emitResetMethod(w *codeWriter, im *importManager, pkg *packages.Package, t targetStruct) error { @@ -279,79 +61,6 @@ func (w *codeWriter) line(indent int, s string) { w.buf.WriteByte('\n') } -type importSpec struct { - Path string - Alias string -} - -type importManager struct { - localPkgPath string - byPath map[string]string - usedAlias map[string]bool -} - -func newImportManager(localPkgPath string) *importManager { - return &importManager{ - localPkgPath: localPkgPath, - byPath: map[string]string{}, - usedAlias: map[string]bool{}, - } -} - -func (im *importManager) qualifier(p *types.Package) string { - if p == nil { - return "" - } - if p.Path() == im.localPkgPath { - return "" - } - if alias, ok := im.byPath[p.Path()]; ok { - return alias - } - - base := p.Name() - alias := base - if im.usedAlias[alias] { - for i := 2; ; i++ { - alias = fmt.Sprintf("%s%d", base, i) - if !im.usedAlias[alias] { - break - } - } - } - im.usedAlias[alias] = true - im.byPath[p.Path()] = alias - return alias -} - -func (im *importManager) typeString(t types.Type) string { - return types.TypeString(t, im.qualifier) -} - -func (im *importManager) sortedImports() []importSpec { - paths := make([]string, 0, len(im.byPath)) - for p := range im.byPath { - paths = append(paths, p) - } - sort.Strings(paths) - - out := make([]importSpec, 0, len(paths)) - for _, p := range paths { - alias := im.byPath[p] - - last := p - if idx := strings.LastIndex(p, "/"); idx >= 0 { - last = p[idx+1:] - } - if alias == last { - alias = "" - } - - out = append(out, importSpec{Path: p, Alias: alias}) - } - return out -} - func emitResetForExpr(w *codeWriter, im *importManager, localPkgPath, expr string, t types.Type) { u := t.Underlying() @@ -472,54 +181,3 @@ func accessibleStruct(t types.Type, localPkgPath string) (*types.Struct, bool) { return nil, false } } - -func hasResetMethod(t types.Type) bool { - ms := types.NewMethodSet(t) - for i := 0; i < ms.Len(); i++ { - sel := ms.At(i) - if sel.Obj().Name() != "Reset" { - continue - } - fn, ok := sel.Obj().(*types.Func) - if !ok { - continue - } - sig, ok := fn.Type().(*types.Signature) - if !ok { - continue - } - if sig.Params().Len() == 0 && sig.Results().Len() == 0 { - return true - } - } - return false -} - -func zeroBasic(b *types.Basic) string { - switch b.Kind() { - case types.Bool: - return "false" - case types.String: - return `""` - case types.UntypedNil: - const nilString = "nil" - return nilString - default: - return "0" - } -} - -func zeroExpr(im *importManager, t types.Type) string { - u := t.Underlying() - - switch u := u.(type) { - case *types.Basic: - return zeroBasic(u) - case *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface, *types.Pointer: - return "nil" - case *types.Struct, *types.Array: - return im.typeString(t) + "{}" - default: - return "nil" - } -} diff --git a/cmd/server/main.go b/cmd/server/main.go index d690dc0..738eef9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "fmt" "log" "net/http" "os" @@ -17,6 +16,7 @@ import ( "github.com/vshulcz/Golectra/internal/domain" "github.com/vshulcz/Golectra/internal/services/audit" "github.com/vshulcz/Golectra/internal/services/metrics" + "github.com/vshulcz/Golectra/pkg/util" "go.uber.org/zap" ) @@ -125,15 +125,6 @@ func buildAuditor(cfg config.ServerConfig, logger *zap.Logger) audit.Publisher { return subject } -func na(v string) string { - if v == "" { - return "N/A" - } - return v -} - func printBuildInfo() { - fmt.Printf("Build version: %s\n", na(buildVersion)) - fmt.Printf("Build date: %s\n", na(buildDate)) - fmt.Printf("Build commit: %s\n", na(buildCommit)) + util.PrintBuildInfo(buildVersion, buildDate, buildCommit) } diff --git a/cmd/staticlint/main.go b/cmd/staticlint/main.go index 4ad06ab..667e89e 100644 --- a/cmd/staticlint/main.go +++ b/cmd/staticlint/main.go @@ -84,18 +84,5 @@ func main() { } analyzers = append(analyzers, nilerr.Analyzer, forcetypeassert.Analyzer, osexitmain.Analyzer) - - multichecker.Main( - filterAnalyzers(analyzers)..., - ) -} - -func filterAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { - var filtered []*analysis.Analyzer - for _, a := range analyzers { - if strings.HasPrefix(a.Name, "Golectra") { - filtered = append(filtered, a) - } - } - return filtered + multichecker.Main(analyzers...) } diff --git a/cmd/staticlint/main_test.go b/cmd/staticlint/main_test.go deleted file mode 100644 index 7fa3e37..0000000 --- a/cmd/staticlint/main_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "testing" - - "golang.org/x/tools/go/analysis" -) - -func TestFilterAnalyzers(t *testing.T) { - tests := []struct { - name string - input []*analysis.Analyzer - expected int - }{ - { - name: "filter Golectra analyzers", - input: []*analysis.Analyzer{ - {Name: "GolectraAnalyzer1"}, - {Name: "GolectraAnalyzer2"}, - {Name: "OtherAnalyzer"}, - }, - expected: 2, - }, - { - name: "no Golectra analyzers", - input: []*analysis.Analyzer{ - {Name: "OtherAnalyzer1"}, - {Name: "OtherAnalyzer2"}, - }, - expected: 0, - }, - { - name: "empty input", - input: []*analysis.Analyzer{}, - expected: 0, - }, - { - name: "all Golectra analyzers", - input: []*analysis.Analyzer{ - {Name: "GolectraAnalyzer1"}, - {Name: "GolectraAnalyzer2"}, - }, - expected: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - filtered := filterAnalyzers(tt.input) - - if len(filtered) != tt.expected { - t.Errorf("expected %d analyzers, got %d", tt.expected, len(filtered)) - } - - for _, a := range filtered { - if a.Name != "GolectraAnalyzer1" && a.Name != "GolectraAnalyzer2" { - t.Errorf("unexpected analyzer: %s", a.Name) - } - } - }) - } -} diff --git a/cmd/staticlint/osexitmain/osexitmain.go b/cmd/staticlint/osexitmain/osexitmain.go index f2caf5e..6110f17 100644 --- a/cmd/staticlint/osexitmain/osexitmain.go +++ b/cmd/staticlint/osexitmain/osexitmain.go @@ -5,6 +5,8 @@ import ( "fmt" "go/ast" "go/types" + "os" + "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -41,6 +43,10 @@ func run(pass *analysis.Pass) (any, error) { case *ast.FuncLit: return false case *ast.CallExpr: + pos := pass.Fset.Position(x.Pos()) + if strings.Contains(pos.Filename, string(os.PathSeparator)+"go-build"+string(os.PathSeparator)) { + return false + } if isOsExitCall(pass, x) { pass.Reportf(x.Pos(), "It is forbidden to call os.Exit directly in main function; use return code from main instead") } diff --git a/cmd/staticlint/osexitmain/osexitmain_test.go b/cmd/staticlint/osexitmain/osexitmain_test.go index 346f58d..5a61942 100644 --- a/cmd/staticlint/osexitmain/osexitmain_test.go +++ b/cmd/staticlint/osexitmain/osexitmain_test.go @@ -117,13 +117,21 @@ func TestIsOsExitCall(t *testing.T) { t.Run(tt.name, func(t *testing.T) { pass := &analysis.Pass{ TypesInfo: &types.Info{ - Uses: map[*ast.Ident]types.Object{ - tt.callExpr.Fun.(*ast.SelectorExpr).Sel: types.NewFunc(0, types.NewPackage("os", "os"), "Exit", types.NewSignatureType(nil, nil, nil, nil, nil, false)), - }, + Uses: map[*ast.Ident]types.Object{}, }, } + + if selector, ok := tt.callExpr.Fun.(*ast.SelectorExpr); ok { + pass.TypesInfo.Uses[selector.Sel] = types.NewFunc(0, types.NewPackage("os", "os"), "Exit", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + } else { + t.Errorf("expected *ast.SelectorExpr, got %T", tt.callExpr.Fun) + } if tt.name == "non os.Exit call" { - pass.TypesInfo.Uses[tt.callExpr.Fun.(*ast.SelectorExpr).Sel] = types.NewFunc(0, types.NewPackage("fmt", "fmt"), "Println", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + if selector, ok := tt.callExpr.Fun.(*ast.SelectorExpr); ok { + pass.TypesInfo.Uses[selector.Sel] = types.NewFunc(0, types.NewPackage("fmt", "fmt"), "Println", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + } else { + t.Errorf("expected *ast.SelectorExpr, got %T", tt.callExpr.Fun) + } } res := isOsExitCall(pass, tt.callExpr) if res != tt.expectRes { diff --git a/internal/misc/pool.go b/internal/misc/pool.go index 81ab788..f17347e 100644 --- a/internal/misc/pool.go +++ b/internal/misc/pool.go @@ -27,7 +27,12 @@ func NewPool[T Resetter](newFn func() T) *Pool[T] { // Get retrieves an object from the pool. func (pl *Pool[T]) Get() T { - return pl.p.Get().(T) + obj := pl.p.Get() + if value, ok := obj.(T); ok { + return value + } + var zero T + return zero } // Put returns an object to the pool after resetting it. diff --git a/pkg/util/common.go b/pkg/util/common.go new file mode 100644 index 0000000..f0a2f20 --- /dev/null +++ b/pkg/util/common.go @@ -0,0 +1,19 @@ +// Package util provides utility functions for the application. +package util + +import "fmt" + +// na returns "N/A" if the input string is empty, otherwise it returns the input string. +func na(v string) string { + if v == "" { + return "N/A" + } + return v +} + +// PrintBuildInfo prints the build version, date, and commit information. +func PrintBuildInfo(buildVersion, buildDate, buildCommit string) { + fmt.Printf("Build version: %s\n", na(buildVersion)) + fmt.Printf("Build date: %s\n", na(buildDate)) + fmt.Printf("Build commit: %s\n", na(buildCommit)) +}