Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 158 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,36 @@ go install github.com/flaticols/resetgen@latest
Or add as a tool dependency (Go 1.24+):

```bash
go get -tool github.com/flaticols/resetgen
go get -tool github.com/flaticols/resetgen@latest
```

### Go 1.24+ Tool Mechanism

Go 1.24 introduced the ability to manage CLI tools as dependencies. You can declare tool requirements in `go.mod`:

```go
tool (
github.com/flaticols/resetgen
)
```

Run with `go tool`:

```bash
# Generate from current package
go tool resetgen

# Generate from specific packages
go tool resetgen ./...
go tool resetgen ./cmd ./internal

# With flags
go tool resetgen -structs User,Order ./...
go tool resetgen -version
```

This approach keeps your tool versions synchronized with your project, just like regular dependencies.

### Usage

Add `reset` tags to your struct fields and run the generator:
Expand Down Expand Up @@ -69,6 +96,136 @@ func (s *Request) Reset() {
| `reset:"value"` | Default value |
| `reset:"-"` | Skip field |

## CLI Flag Syntax

### `-structs` Flag

Specify which structs to generate using the `-structs` flag:

```bash
//go:generate resetgen -structs User,Order,Config

# Or with multiple files
resetgen -structs User,Order,Config ./...
```

When `-structs` is specified:
- **ONLY** the listed structs are processed (tags and directives are ignored for struct selection)
- All exported fields are reset to zero values
- Field-level `reset` tags still work for custom values or to skip specific fields

**Example:**
```go
//go:generate resetgen -structs User,Order

type User struct {
ID int64
Name string
Secret string `reset:"-"` // Still respected - field will not be reset
}

type Order struct {
ID int64
Items []string
Total float64 `reset:"0.0"` // Custom value still works
}

type Logger struct {
Level string // Will NOT be generated (not in -structs list)
}
```

### Package-Qualified Names

When you have structs with the same name in different packages, use package-qualified names:

```bash
# Process User in models package only
resetgen -structs models.User ./...

# Process User in both models and api packages
resetgen -structs models.User,api.User ./...

# Mix simple and qualified names
resetgen -structs Order,models.User ./...
```

**Rules:**
- Simple name (`User`) → processes ALL User structs in all packages
- Qualified name (`models.User`) → processes only User in models package
- Package path uses Go import path format (lowercase with dots/slashes)
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "Package path uses Go import path format (lowercase with dots/slashes)" which suggests full import paths are supported. However, the implementation in main.go (lines 59-60) only accepts qualified names with exactly one dot, splitting into two parts (package.Struct). This means full import paths like "github.com/user/pkg.Struct" are not supported.

The documentation should clarify that only simple package names are supported for qualification, not full import paths. For example: "Package name must be a simple package identifier (e.g., 'models', 'api', not full import paths)".

Suggested change
- Package path uses Go import path format (lowercase with dots/slashes)
- Package name must be a simple package identifier (e.g., 'models', 'api'), not a full import path (e.g., 'github.com/user/models')

Copilot uses AI. Check for mistakes.

**Example with multiple packages:**
```go
// models/user.go
//go:generate resetgen -structs models.User,api.User

package models

type User struct {
ID int64 `reset:""`
Name string `reset:""`
Email string `reset:""`
}

// api/user.go
//go:generate resetgen -structs models.User,api.User

package api

type User struct {
ID string `reset:""`
Status string `reset:"active"`
}
```

Both packages can use the same go:generate directive with package-qualified names, and each will generate only its own Reset() method.

## Directive Syntax

Use the `+resetgen` comment directive to mark structs for automatic `Reset()` generation without tagging every field:

```go
//go:generate resetgen

package main

// +resetgen
type Request struct {
ID string // defaults to zero value
Method string // defaults to zero value
Headers map[string]string // defaults to zero value
Secret string `reset:"-"` // skipped from reset
}
```

Generated `request.gen.go`:

```go
func (s *Request) Reset() {
s.ID = ""
s.Method = ""
clear(s.Headers) // preserves capacity
// Secret is not reset (reset:"-")
}
```

### How Directive Works

- **Struct Selection**: Structs are processed if they have a `+resetgen` comment OR contain `reset` tags
- **Field Processing**: All exported fields are reset to zero values
- **Custom Values**: Fields with explicit `reset` tags use their specified values
- **Skip Fields**: Use `reset:"-"` to exclude specific fields from reset
- **Unexported Fields**: Private fields (lowercase) are automatically skipped for safety

### Directive Formats

All of these are recognized:
- `//+resetgen`
- `// +resetgen`
- `// +resetgen`
- `/* +resetgen */`

## Features

- **Allocation-free** — slices truncate (`s[:0]`), maps clear (`clear(m)`)
Expand Down
12 changes: 3 additions & 9 deletions cmd/resetgen-analyzer/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ var Analyzer = &analysis.Analyzer{
Run: run,
}

// run performs the analysis on all function declarations and literals in the pass.
func run(pass *analysis.Pass) (any, error) {
insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

// Analyze each function separately
nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
(*ast.FuncLit)(nil),
Expand All @@ -45,11 +45,11 @@ func run(pass *analysis.Pass) (any, error) {
return nil, nil
}

// analyzeFunction checks a function body for sync.Pool.Put() calls without preceding Reset() calls.
// Tracks which variables have had Reset() called on them and reports violations.
func analyzeFunction(pass *analysis.Pass, body *ast.BlockStmt) {
// Track variables that had Reset() called on them
resetCalled := make(map[string]bool)

// Walk statements in order
ast.Inspect(body, func(n ast.Node) bool {
stmt, ok := n.(*ast.ExprStmt)
if !ok {
Expand All @@ -66,15 +66,13 @@ func analyzeFunction(pass *analysis.Pass, body *ast.BlockStmt) {
return true
}

// Check for x.Reset() calls - track any variable that had Reset called
if sel.Sel.Name == "Reset" && len(call.Args) == 0 {
varName := extractVarName(sel.X)
if varName != "" {
resetCalled[varName] = true
}
}

// Check for sync.Pool.Put(x) calls
if sel.Sel.Name == "Put" && isSyncPoolMethod(sel, pass.TypesInfo) {
if len(call.Args) == 1 {
varName := extractVarName(call.Args[0])
Expand All @@ -88,22 +86,18 @@ func analyzeFunction(pass *analysis.Pass, body *ast.BlockStmt) {
})
}

// extractVarName gets the variable name from an expression
// Handles: x, s.x, s.field.x
func extractVarName(expr ast.Expr) string {
switch e := expr.(type) {
case *ast.Ident:
return e.Name
case *ast.SelectorExpr:
// For s.field, we still track by the root identifier
return extractVarName(e.X)
case *ast.StarExpr:
return extractVarName(e.X)
}
return ""
}

// isSyncPoolMethod checks if sel is a method on sync.Pool
func isSyncPoolMethod(sel *ast.SelectorExpr, info *types.Info) bool {
tv, ok := info.Types[sel.X]
if !ok {
Expand Down
13 changes: 11 additions & 2 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
"github.com/flaticols/resetgen/internal/types"
)

// Generate produces Reset() methods for all structs in the file info.
// Generate produces Reset() methods for all structs in the file info, including package
// declaration, imports, and all Reset() method implementations.
func Generate(info *types.FileInfo) string {
if len(info.Structs) == 0 {
return ""
Expand Down Expand Up @@ -44,14 +45,16 @@ func Generate(info *types.FileInfo) string {
return b.String()
}

// GenerateStruct produces a Reset() method for a single struct.
// GenerateStruct generates a single Reset() method for a struct.
func GenerateStruct(s *types.StructInfo) string {
var b strings.Builder
b.Grow(512)
generateReset(&b, s)
return b.String()
}

// collectImports extracts all required standard library imports from struct fields.
// Maps common package aliases to their full import paths.
func collectImports(structs []types.StructInfo) []string {
pkgSet := make(map[string]bool)

Expand All @@ -73,6 +76,7 @@ func collectImports(structs []types.StructInfo) []string {
return imports
}

// extractPackage maps package aliases to their full import paths for standard library types.
func extractPackage(typeStr string) string {
t := strings.TrimPrefix(typeStr, "*")
idx := strings.Index(t, ".")
Expand Down Expand Up @@ -150,6 +154,8 @@ func generateFieldReset(b *strings.Builder, f *types.FieldInfo) {
}
}

// generateDefaultReset writes the code to reset a field to its default value.
// For slices/maps and embedded structs, delegates to appropriate zero-reset logic.
func generateDefaultReset(b *strings.Builder, f *types.FieldInfo, accessor string) {
switch f.Kind {
case types.KindSlice, types.KindMap:
Expand Down Expand Up @@ -181,6 +187,8 @@ func generateDefaultReset(b *strings.Builder, f *types.FieldInfo, accessor strin
}
}

// generateZeroReset writes the code to reset a field to its zero value.
// Handles embedded types by calling their Reset(), slices by truncating, and maps by clearing.
func generateZeroReset(b *strings.Builder, f *types.FieldInfo, accessor string) {
if f.IsEmbedded {
if isExternalType(f.TypeStr) {
Expand Down Expand Up @@ -299,6 +307,7 @@ func formatDefault(f *types.FieldInfo) string {
}
}

// zeroValue returns the zero value literal for a Go type.
func zeroValue(typeStr string) string {
switch typeStr {
case "string":
Expand Down
Loading