exhaustruct

repository·master·Indexed 41 days ago

https://github.com/gaijinentertainment/go-exhaustruct

A Go analyzer designed to find struct literals with uninitialized fields to ensure complete initialization across a codebase. It supports implicit and explicit modes of operation, comment directives for fine-grained control, and regex-based type selection via CLI flags.

Tokens
3K
Snippets
12
Records
17
Agent score
3%

What's inside exhaustruct

  1. How embedded fields and promoted names work

    master

    Since Go 1.27, composite literals can name promoted fields directly. exhaustruct follows this behavior:

    1. Promoted names: If a promoted name reaches a field, that field is reported individually.
    2. Unreachable promoted names: If a promoted name does not reach a field, the entire embedded field is reported as a single missing unit.
    3. Directive Priority: A directive like //exhaustruct:enforce on a field inside an embedded struct outranks the directive on the parent struct.
    4. Optional Embedded Fields: If an embedded field is marked //exhaustruct:optional, exhaustruct does not descend into it, meaning its promoted fields are also treated as optional.
    type Inner struct {
        ID string
    
        //exhaustruct:enforce
        Must string
    }
    
    //exhaustruct:optional
    type Holder struct {
        Inner
        Port int
    }
    
    //exhaustruct:optional
    type Own struct {
        //exhaustruct:optional
        Inner
        Port int
    }
    
    func example() {
        _ = Holder{Port: 8080} // missing field Must
        _ = Own{Port: 8080}    // OK: Inner is optional in its own right
    }
  2. How exhaustruct handles error returns

    master

    To prevent false positives in error handling, exhaustruct automatically allows empty struct literals in return statements if they are accompanied by a non-nil error value.

    func LoadConfig() (Config, error) {
        if err := validate(); err != nil {
            return Config{}, err // OK: error return
        }
        return Config{Host: "localhost", Port: 8080}, nil
    }
  3. Understand directive priority

    master

    When multiple directives or patterns apply, they are resolved in this order (highest priority first):

    1. Literal //exhaustruct:ignore
    2. Literal //exhaustruct:enforce
    3. Type-level ignore (directive or -ignore-rx pattern)
    4. Type-level enforce (directive or -enforce-rx pattern)
    5. Mode default (implicit=check, explicit=skip)

    Note on conflicts: If two directives target the same line (e.g., //exhaustruct:ignore,enforce), it is treated as a conflict and reported rather than resolved.

  4. How blank fields (_) are treated

    master

    Blank fields (_) have no name that a keyed literal can write to. Consequently, a keyed or empty literal is never reported for leaving them out. However, positional literals must still supply a value for them to satisfy Go's requirements.

    type NoCompare struct {
        _ [0]func()
        A int
    }
    
    func example() {
        _ = NoCompare{A: 1}          // OK
        _ = NoCompare{}              // missing field A
        _ = NoCompare{[0]func(){}, 1} // OK
    }
  5. How unexported fields are treated

    master

    Fields that are unexported and belong to external packages are never required. Since they cannot be initialized from outside their own package, exhaustruct ignores them.

    import "external/pkg"
    
    func example() {
        // If pkg.Server has unexported fields, they are not required
        _ = pkg.Server{Host: "localhost"} // OK
    }
  6. How type parameters are checked

    master

    A literal of a type parameter is checked against the struct that its constraint's terms share. The error is reported under the parameter's name. Field-level directives on the constraint's terms apply. Note that a type parameter itself carries no type-level directive because it is declared in a signature.

    type configish interface {
        ~struct {
            Host string
            Port int
        }
    }
    
    func New[T configish]() T {
        return T{Host: "localhost"} // T is missing field Port
    }
  7. Understand Implicit vs Explicit modes of operation

    master

    Exhaustruct operates in two modes:

    1. Implicit Mode (Default): All struct literals are checked for complete initialization. You must use ignore patterns or directives to exclude specific types.
    2. Explicit Mode: Enabled via the -explicit flag. The analyzer only checks structs that are explicitly marked for enforcement using the //exhaustruct:enforce directive or matched via -enforce-rx patterns. This is ideal for opt-in checking in large codebases.
  8. How type aliases and derived types inherit directives

    master

    Directives behave differently depending on whether they are applied to a field or a type:

    • Field-level directives (//exhaustruct:optional, //exhaustruct:enforce on fields): These are inherited by all type aliases and derived types using that underlying struct.
    • Type-level directives (//exhaustruct:enforce, //exhaustruct:ignore, //exhaustruct:optional on the type definition): These are not inherited. Each alias or derived type must have its own type-level directive to be checked independently.
    //exhaustruct:enforce
    type Config struct {
        Host string
        //exhaustruct:optional
        Timeout int
    }
    
    type MyConfig = Config      // alias: inherits optional Timeout, but NOT enforce
    type ExtConfig Config       // derived: inherits optional Timeout, but NOT enforce
    
    func example() {
        // Config is enforced (has type-level directive)
        _ = Config{} // ERROR in explicit mode
    
        // MyConfig inherits field optionality but not type enforcement
        _ = MyConfig{Host: "localhost"} // OK: Timeout is optional (inherited)
        _ = MyConfig{}                  // OK in explicit mode: type not enforced
    }
  9. How exhaustruct works

    master

    The exhaustruct analyzer inspects struct literals and reports when required fields are not initialized.

    Example behavior:

    type User struct {
        Name  string
        Email string
        Age   int
    }
    
    func example() {
        _ = User{Name: "alice"} // ERROR: missing fields Email, Age
        _ = User{Name: "alice", Email: "alice@example.com", Age: 30} // OK
    }
  10. Migrate from v4 to v5: Struct Tags to Comment Directives

    master

    Struct tags (e.g., exhaustruct:"optional") are deprecated in v5. You should use comment directives instead.

    Manual Migration:

    // v4 (deprecated)
    type Server struct {
        Host    string
        Timeout int `exhaustruct:"optional"`
    }
    
    // v5
    type Server struct {
        Host    string
        //exhaustruct:optional
        Timeout int
    }

    Automatic Migration: You can use the -fix flag to automatically convert deprecated struct tags into comment directives.

    exhaustruct -fix ./...
  11. Migrate from v4 to v5: Flag Renames

    master

    In v5, flags have been renamed and their behavior separated. Specifically, v4's -include-rx combined scope and selection, whereas v5 separates them using -explicit (scope) and -enforce-rx (selection).

    v4 Flagv5 FlagDescription
    -include-rx / -i-enforce-rxSelects types to enforce
    -exclude-rx / -e-ignore-rxSelects types to ignore

    Migration Example: To convert a v4 pattern to v5, you must add the -explicit flag to ensure you aren't in implicit mode (where every type is checked).

    # v4
    exhaustruct -include-rx '.*\.Config' ./...
    
    # v5
    exhaustruct -explicit -enforce-rx '.*\.Config' ./...