Skip to content

cmplint is a Go linter that detects comparisons against the address of newly created values.

License

Notifications You must be signed in to change notification settings

fillmore-labs/cmplint

Repository files navigation

cmplint

Go Reference Test CodeQL Coverage Go Report Card License

cmplint is a Go linter that detects comparisons like ptr == &MyStruct{} — code that is almost always incorrect. Each composite literal or new() call allocates a unique address, so these comparisons typically evaluate to false or have undefined behavior when zero-sized types are involved.

Installation

Go

go install fillmore-labs.com/cmplint@latest

Homebrew

brew install fillmore-labs/tap/cmplint

Eget

Eget downloads prebuilt binaries. After installing it:

eget fillmore-labs/cmplint

Usage

cmplint ./...

The Problem

Comparing pointers to newly allocated values is a source of subtle bugs in Go. Consider:

import (
  "time"

  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

  d := &metav1.Duration{30 * time.Second}
  if (d == &metav1.Duration{30 * time.Second}) { // This will always be false!
    // This code never executes
  }

The reason: every &T{} or new(T) expression creates a fresh allocation with a unique address. So ptr == &MyStruct{} is almost always false, regardless of what ptr points to.

For zero-sized types, the behavior is undefined:

"Pointers to distinct zero-size variables may or may not be equal."

Examples of Problematic Code

Here are examples that cmplint will flag:

Direct Pointer Comparisons

import (
  "github.com/operator-framework/api/pkg/operators/v1alpha1"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Checking if a operator update strategy matches expected values
func validateUpdateStrategy(spec *v1alpha1.CatalogSourceSpec) {
  expectedTime := 30 * time.Second

  // This comparison will always be false - &metav1.Duration{} creates a unique address.
  if (spec.UpdateStrategy.Interval != &metav1.Duration{Duration: expectedTime}) {
    // ...
  }

  // Correct approach: Dereference the pointer and compare values (after a nil check).
  if spec.UpdateStrategy.Interval == nil || spec.UpdateStrategy.Interval.Duration != expectedTime {
    // ...
  }
}

Error Handling with errors.Is

func connectToDatabase() {
  db, err := dbConnect()
  // This will always be false - &url.Error{} creates a unique address.
  if errors.Is(err, &url.Error{}) {
    log.Fatal("Cannot connect to DB")
  }

  // Correct approach:
  var urlErr *url.Error
  if errors.As(err, &urlErr) {
    log.Fatal("Error connecting to DB:", urlErr)
  }
  // ...
}

func unmarshalEvent(msg []byte) {
  var es []cloudevents.Event
  err := json.Unmarshal(msg, &es)
  // This comparison will always be false:
  if errors.Is(err, &json.UnmarshalTypeError{}) {
    //...
  }

  // Correct approach:
  var typeErr *json.UnmarshalTypeError
  if errors.As(err, &typeErr) {
    //...
  }
}

Special Cases

errors.Is and Similar Functions

cmplint suppresses diagnostics when error types implement Unwrap or Is methods, since these enable legitimate use with errors.Is. Specifically, diagnostics are suppressed when:

  • The error type has an Unwrap() error method, as errors.Is traverses the error tree.
Unwrap() error tree example.
type wrappedError struct{ Cause error }

func (e *wrappedError) Error() string { return "wrapped: " + e.Cause.Error() }
func (e *wrappedError) Unwrap() error { return e.Cause } // This suppresses the diagnostic.

  // No warning for this code:
  if errors.Is(&wrappedError{os.ErrNoDeadline}, os.ErrNoDeadline) { // Valid due to "Unwrap" method.
    // ...
  }
  • The error type has an Is(error) bool method, as custom comparison logic is executed.
Custom Is(error) bool method example.

When the static type of an error is just the error interface, the analyzer cannot know its dynamic type. Therefore, the diagnostic is also suppressed when the target has an Is(error) bool method:

type customError struct{ Code int }

func (i *customError) Error() string { return fmt.Sprintf("custom error %d", i.Code) }

func (i *customError) Is(err error) bool { // This suppresses the diagnostic.
  _, ok := err.(*customError)
  return ok
}

  err = func() error {
    return &customError{100}
  }()

  // No warning for this code:
  if errors.Is(err, &customError{200}) { // Valid due to custom "Is" method.
    // ...
  }

Rare False Positives

The applied heuristic can lead to false positives in rare cases. For example, if one error type's Is method is designed to compare against a different error type, cmplint may flag valid code. This pattern is uncommon and potentially confusing.

This workaround improves clarity and suppresses the linting error.
type errorA struct{ Code int }

func (e *errorA) Error() string { return fmt.Sprintf("error a %d", e.Code) }

type errorB struct{ Code int }

func (e *errorB) Error() string { return fmt.Sprintf("error b %d", e.Code) }

func (e *errorB) Is(err error) bool {
  if err, ok := err.(*errorA); ok { // errorB knows how to check against errorA.
    return e.Code == err.Code
  }

  return false
}

  err := func() error {
    return &errorB{100}
  }()

  // This valid code gets flagged:
  if errors.Is(err, &errorA{100}) { // Flagged, but technically correct.
    // ...
  }

  // Document to clarify intent and assign to an identifier to suppress the warning:
  target := &errorA{100} // errorB's "Is" method should match.
  if errors.Is(err, target) {
    // ...
  }

Diagnostics

  • “Result of comparison with address of new variable of type "..." is always false”

    This indicates a comparison like ptr == &MyStruct{} that will never be true. Consider these fixes:

    • Compare values instead:

        *ptr == MyStruct{}
    • Use errors.As for errors:

        var target *MyError
        if errors.As(err, &target) {
          // ...
        }
    • Check dynamic type (for interface types):

        if v, ok := v.(*MyStruct); ok {
          if v.SomeField == expected { /* ... */ }
        }
    • Pre-declare the target:

        var sentinel = &MyStruct{}
        // ...
        if ptr == sentinel { /* ... */ }
  • “Result of comparison with address of new variable of type "..." is false or undefined”

    This diagnostic appears for zero-sized types where the comparison behavior is undefined:

    type Skip struct{}
    
    func (e *Skip) Error() string { return "host hook execution skipped." }
    
    func (r renderRunner) RunHostHook(ctx context.Context, hook *hostHook) {
      if err := hook.run(ctx /*, ... */); errors.Is(err, &Skip{}) { // Undefined behavior.
        // ...
      }
    }

    or

        defer func() {
          err := recover()
    
          if err, ok := err.(error); ok &&
            errors.Is(err, &runtime.PanicNilError{}) { // Undefined behavior.
            log.Print("panic called with nil argument")
          }
        }()
    
        panic(nil)

    While this might work due to Go runtime optimizations, its logic is unsound. Use errors.As instead:

      var panicErr *runtime.PanicNilError
      if errors.As(err, &panicErr) {
        log.Println("panic error")
      }

Integration

go vet

go vet -vettool=$(which cmplint) ./...

errortype

errortype is a comprehensive error handling linter that includes cmplint's checks. If you're already using errortype, you don't need cmplint separately — though running cmplint alone might be faster on large codebases.

golangci-lint Module Plugin

Add a .custom-gcl.yaml file to your project root:

---
version: v2.8.0
plugins:
  - module: fillmore-labs.com/cmplint
    import: fillmore-labs.com/cmplint/gclplugin
    version: v0.0.6

See also the golangci-lint module plugin system documentation.

CI/CD Integration

Add cmplint to your CI pipeline:

# GitHub Actions example
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
        with:
          go-version: 1.25
      - name: Run cmplint
        run: go run fillmore-labs.com/cmplint@latest ./...

Links

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

cmplint is a Go linter that detects comparisons against the address of newly created values.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages