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/.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" + } +} 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/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) +} 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