Skip to content

Latest commit

 

History

102 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sigfmt — Linter for Go Function Signatures

Go Reference GitHub Release Go Version CI codecov Go Report Card License: MIT

sigfmt is a golangci-lint plugin and Go function formatter that automatically checks and formats function, method, and field signatures — enforcing consistent code style across your Go codebase. It collapses multi-line signatures that fit on one line and packs long signatures compactly, keeping diffs clean and code readable.

🎯 Motivation

The Problem

The standard gofmt formatter provides basic formatting but leaves freedom of choice when breaking long function signatures across multiple lines. This often leads to inconsistency, poor readability, and polluted git history.

1. Polluted git history: Unnecessary changes appear in diffs when different developers format code differently.

- func Save(data []byte,
-     path string) error {
+ func Save(data []byte, path string) error {

2. Bloated interfaces and structs: Without parameter packing, interfaces and structs take up excessive vertical space.

// Takes 16 lines for just three simple methods!
type UserService interface {
    Create(
        name string,
        email string,
        age int,
    ) error
    // ...
}

The Solution

sigfmt solves these problems by enforcing strict but reasonable rules:

  • Compactness: If a signature fits on one line (considering the limit), it must be on one line.
  • Structure: If a signature is long, it should be formatted to use vertical space most efficiently.
  • Automation: The linter points out problems and suggests automatic fixes.

🚀 Features

The linter analyzes:

  • Function declarations (func Foo(...))
  • Type methods (func (s *S) Bar(...))
  • Anonymous functions / Literals (var f = func(...))
  • Methods in interfaces (type I interface { Method(...) })
  • Struct fields with function type (type S struct { Callback func(...) })
  • Generics (Go 1.18+): Correct handling of type parameters [T any].

🛠 Installation

sigfmt is a plugin for golangci-lint v2 (it uses the plugin-module-register module API introduced in v2.0.0). You build a custom golangci-lint binary that includes the plugin, either from a released module version or from a local checkout.

Option 1: From a released version (Recommended)

  1. Create a .custom-gcl.yml file in your project root:

    version: v2.12.2 # any golangci-lint v2 release, see matrix below
    plugins:
      - module: 'github.com/vsfedorenko/sigfmt'
        version: v1.5.0 # Replace with the latest released tag
  2. Run the command to build the custom binary:

    golangci-lint custom

    This downloads sigfmt from the Go module proxy and produces a custom-gcl binary in the current directory.

Option 2: From a local checkout

Useful while developing the plugin, or to test an unreleased commit:

  1. Create a .custom-gcl.yml next to your project (the path is relative to it):
    version: v2.12.2
    plugins:
      - module: 'github.com/vsfedorenko/sigfmt'
        path: ../sigfmt # relative path to the plugin module root
  2. Run golangci-lint custom.

Supported golangci-lint versions

sigfmt targets the v2 module-plugin API. Both build paths were exercised end-to-end (plugin loads, diagnostics produced, --fix applies, settings decoded):

golangci-lint plugin API From proxy Local path Notes
v2.7.1 plugin-module-register ✅ ✅ Oldest v2 tested
v2.12.2 plugin-module-register ✅ ✅ Go ≥ 1.24
v2.13.1 plugin-module-register ✅ ✅ Newest v2 tested; requires Go ≥ 1.26 to build the custom binary

The From-proxy column was verified against the published v1.5.0 tag (go run …/golangci-lint@v2.13.1 custom with version: v1.5.0 in .custom-gcl.yml), the local-path column against a path: checkout — both produce a working custom binary: diagnostics fire, --fix applies, a re-run is clean.

v1 (golangci-lint ≤ 1.x) is not supported: the v1 plugin API predates plugin-module-register. If golangci-lint custom reports an unknown command, upgrade golangci-lint to v2 first.

Build note (Linux, newer binutils): if the custom build fails at link time with collect2: fatal error: cannot find 'ld' and the gcc command line contains -fuse-ld=gold, your binutils no longer ships the gold linker (dropped in binutils ≥ 2.44 on some distros). Build with CGO_ENABLED=0 — the linter is pure Go and needs no cgo:

CGO_ENABLED=0 golangci-lint custom

⚙️ Configuration

Configure the linter in your project's .golangci.yml v2 format (note the top-level version: "2" and linters.settings nesting — a v1-style top-level linters-settings key is rejected by golangci-lint v2 with unsupported version of the configuration).

Parameters (under linters.settings.custom.sigfmt.settings):

  • max-line-len (int): Maximum allowed line length. Default: 120.
  • tab-width (int): Tab width for visual calculation. Default: 8.
  • pack-struct-fields (bool): Enable packing of struct fields. Default: true.
  • pack-interface-methods (bool): Enable packing of interface methods. Default: true.
  • param-groups (list of lists): Define groups of parameter types that should be kept together on the same line.
  • ignore-tests (bool): Skip _test.go files entirely. Test files are frequently table-driven with intentionally wide signatures; many teams prefer formatting them manually. Default: false.

Generated files are skipped automatically. Files carrying the conventional Code generated ... DO NOT EDIT. header before the package clause (e.g. *.pb.go output, mocks, zz_* stringers) produce no diagnostics — generated code is not hand-maintained. There is no setting for this: it always applies, matching the behaviour of golangci-lint core linters.

Build-excluded files are skipped too. A file whose build constraint excludes it from the current build (//go:build ignore, //go:build windows on Linux) is skipped even when passed directly as a file argument (sigfmt gen.go) — the same tolerance go vet has. Constraints are evaluated with the toolchain's default context, so //go:build linux stays lintable on Linux. Files with //line directives are processed normally: the directive text survives any suggested fix.

Example .golangci.yml (verified against a custom v2.12.2 binary — including the param-groups shape below):

version: "2"

linters:
  default: none
  enable:
    - sigfmt
  settings:
    custom:
      sigfmt:
        type: "module"
        description: "Advanced function signature formatter"
        settings:
          max-line-len: 120
          tab-width: 8
          pack-struct-fields: true
          pack-interface-methods: true
          param-groups:
            - ["context.Context", "*sql.Tx"] # Group ctx and tx together
            - ["context.Context"]            # Ensure ctx is on its own line (if no tx)

💡 Usage

As a golangci-lint plugin

To run the linter, use your custom binary:

# Run analysis
./custom-gcl run

# Automatically fix issues
./custom-gcl run --fix

Standalone CLI

The sigfmt binary can also be used directly without golangci-lint. Release builds are published for linux, darwin, and windows on amd64/arm64:

# From a release archive (see https://github.com/vsfedorenko/sigfmt/releases)
curl -sSL https://github.com/vsfedorenko/sigfmt/releases/latest/download/sigfmt_linux_amd64.tar.gz | tar xz
sudo install sigfmt /usr/local/bin/

# Or build from source (any Go >= 1.25 toolchain)
go install github.com/vsfedorenko/sigfmt/cmd/sigfmt@latest

Homebrew (macOS / Linux)

The repository doubles as a Homebrew tap — the formula installs the prebuilt release binary:

brew tap vsfedorenko/sigfmt https://github.com/vsfedorenko/sigfmt
brew install vsfedorenko/sigfmt/sigfmt

The formula declares go as a test-only dependency: the analyzer shells out to the go toolchain at runtime even in single-file mode, but your own project toolchain is used for real runs. Updates land automatically: every release regenerates Formula/sigfmt.rb from the published checksums and commits it to main.

Then run it on your project:

# Run analysis on a package
sigfmt ./...

# Automatically fix issues in-place
sigfmt -fix ./...

# Preview the changes a -fix would make, without writing anything
sigfmt -diff ./...

# With custom settings
sigfmt -max-line-len 100 -tab-width 4 ./...

# With parameter groups
sigfmt -param-groups "context.Context,error;io.Reader,io.Writer" ./...

Pre-commit integration

The repo ships .pre-commit-hooks.yaml. Add to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/vsfedorenko/sigfmt
    rev: v1.5.0  # use the latest released tag
    hooks:
      - id: sigfmt        # check only, blocks the commit on violations
      - id: sigfmt-fix    # manual stage: pre-commit run --hook-stage manual sigfmt-fix

Available CLI flags:

Flag Default Description
-max-line-len 120 Maximum line length before multi-line signatures are required
-tab-width 8 Visual width of a tab character for length calculations
-pack-struct-fields true Aggressively pack function-type struct fields
-pack-interface-methods true Aggressively pack method signatures in interfaces
-param-groups (none) Semicolon-separated parameter type groups (e.g. "context.Context,error;io.Reader,io.Writer")
-fix false Apply suggested fixes automatically (provided by singlechecker)
-diff false Preview fixes as a unified diff without applying them. Works standalone (sigfmt -diff ./..., like gofmt -d) or combined with -fix; a lone -diff is promoted to -fix -diff by the sigfmt entry point
-V (version) Print analyzer version and exit

Editor integration

The standalone CLI edits a single file in place (sigfmt -fix file.go), which makes format-on-save easy to wire up. sigfmt is not a language server, and gopls does not run third-party analyzers, so the pattern is always the same: invoke the binary on the saved file and reload the buffer.

VS Code — with the Run on Save extension, append to settings.json:

{
  "emeraldwalk.runonsave": {
    "commands": [
      {
        "match": "\\.go$",
        "cmd": "sigfmt -fix '${file}'"
      }
    ]
  }
}

Neovim — format the current buffer on save:

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = "*.go",
  callback = function(args)
    -- sigfmt -fix edits the file in place; reload it into the buffer.
    vim.fn.system({ "sigfmt", "-fix", vim.api.nvim_buf_get_name(args.buf) })
    vim.cmd("edit!")
  end,
})

Other editors — if your editor cannot run a command on save, use the pre-commit hooks above: sigfmt-fix on the manual stage (pre-commit run --hook-stage manual sigfmt-fix) applies the same fixes before every commit.

🧠 How It Works

The linter analyzes function signatures using the Go AST (Abstract Syntax Tree) and applies a two-stage formatting strategy:

1. Collapse Stage

Goal: Maximize compactness for short signatures.

The linter calculates the visual width of a signature if it were on a single line. If it fits within max-line-len (default 120), the signature must be collapsed.

Example:

- func Sum(
-     a int,
-     b int,
- ) int { ... }
+ func Sum(a int, b int) int { ... }

Diagnostic Message: "Signature can be formatted more compactly" Suggested Fix Message: "Format signature"

This applies to:

  • Function declarations
  • Type methods (with receivers)
  • Anonymous functions / closures
  • Interface methods
  • Struct fields with function types

Comment Preservation

Signatures containing comments (// or /* */) inside the rewritten range are left untouched: the renderer rebuilds signatures from the AST, and a rewrite would silently drop those comments. Doc comments above a signature are outside the rewrite range and never affected — such signatures are formatted normally. This invariant is enforced by a black-box test (TestCommentPreservationZeroLoss) over a corpus of commented signatures in unusual positions: applying all suggested fixes must lose zero comments and keep the file parseable.

2. Reformat / Packing Stage

Goal: Optimize vertical space for long signatures.

If a signature doesn't fit on one line, the linter applies context-aware packing strategies. The diagnostic message for this stage is the same as for the collapse stage: "Signature can be formatted more compactly", with the fix message "Format signature".

A. Regular Functions (Conservative)

Preserves logical parameter grouping. Uses minimal reformatting to respect developer intent.

  func ProcessData(
-     param1 string,
-     param2 string,
-     param3 int,
-     param4 bool,
  ) error {
+ func ProcessData(
+     param1 string, param2 string,
+     param3 int, param4 bool,
+ ) error {
      // ...
  }
B. Interfaces & Structs (Aggressive)

Packs multiple parameters per line to minimize vertical space bloat. Interface definitions often have many similar methods, so aggressive packing significantly improves readability.

  type Logger interface {
-     Log(
-         level Level,
-         msg string,
-         args ...interface{},
-     )
+     Log(level Level, msg string, args ...interface{})

-     Error(
-         msg string,
-         err error,
-     )
+     Error(msg string, err error)
  }
C. Parameter Groups (Advanced)

When configured with param-groups, the linter keeps semantically related parameters together on the same line:

param-groups:
  - ["context.Context", "*sql.Tx"]  # Always group ctx and tx
  - ["context.Context"]              # If no tx, keep ctx on its own line
- func Query(
-     ctx context.Context,
-     tx *sql.Tx,
-     sql string,
-     args ...interface{},
- ) error {
+ func Query(
+     ctx context.Context, tx *sql.Tx,
+     sql string, args ...interface{},
+ ) error {
      // ...
  }

Width Calculation

The linter calculates visual width considering:

  • Tab expansion (tab-width, default 8)
  • Receiver length (for methods)
  • Type parameter length (for generics)
  • Return type length
  • Comment preservation: signatures with internal comments are skipped entirely (see Comment Preservation above)

Example:

// Visual width = len("func Map[T any, R any](items []T, fn func(T) R) []R")
func Map[T any, R any](items []T, fn func(T) R) []R  // 54 chars (fits in 120)

📸 Examples Gallery

1. Basic Function Collapsing

Simple Functions:

- func ShortFunction(
-     a int,
-     b string,
- ) error {
+ func ShortFunction(a int, b string) error {
      return nil
  }

- func Sum(
-     nums ...int,
- ) int {
+ func Sum(nums ...int) int {
      total := 0
      for _, n := range nums {
          total += n
      }
      return total
  }

Multiple Return Values:

- func MultipleReturns(
-     x int,
-     y int,
- ) (int, error) {
+ func MultipleReturns(x int, y int) (int, error) {
      return x + y, nil
  }

- func NamedReturns(
-     a int,
-     b int,
- ) (sum int, err error) {
+ func NamedReturns(a int, b int) (sum int, err error) {
      return a + b, nil
  }

Mixed Parameters (shorthand notation):

- func MixedParams(
-     a, b int,
-     c string,
- ) error {
+ func MixedParams(a, b int, c string) error {
      return nil
  }

2. Methods

Type Methods:

  type Calculator struct{}

- func (c *Calculator) Add(
-     a int,
-     b int,
- ) int {
+ func (c *Calculator) Add(a int, b int) int {
      return a + b
  }

Anonymous Functions / Closures:

- var myFunc = func(
-     a int,
-     b int,
- ) int {
+ var myFunc = func(a int, b int) int {
      return a + b
  }

  group.Go(
-     func(
-         ctx context.Context,
-     ) error {
+     func(ctx context.Context) error {
          // ...
      },
  )

3. Interfaces (Aggressive Packing)

Simple Interface Methods:

  type MyInterface interface {
-     Method(
-         ctx context.Context,
-     ) error
+     Method(ctx context.Context) error

-     Get(
-         id string,
-     ) error
+     Get(id string) error

-     GetMultiple(
-         id string,
-     ) (string, error)
+     GetMultiple(id string) (string, error)
  }

Complex Interfaces (Packing Multiple Parameters Per Line):

  type ComplexInterface interface {
-     ProcessWithVeryLongNameAndManyParameters(
-         parameterOne string,
-         parameterTwo string,
-         parameterThree string,
-         parameterFour string,
-     ) error
+     ProcessWithVeryLongNameAndManyParameters(parameterOne string, parameterTwo string,
+         parameterThree string, parameterFour string) error

-     ProcessManyParams(
-         parameterOne string,
-         parameterTwo string,
-         parameterThree string,
-         parameterFour string,
-         parameterFive string,
-         parameterSix string,
-         parameterSeven string,
-         parameterEight string,
-     ) error
+     ProcessManyParams(parameterOne string, parameterTwo string, parameterThree string,
+         parameterFour string, parameterFive string, parameterSix string,
+         parameterSeven string, parameterEight string) error
  }

Service Interfaces (Real-World Example):

- type UserRepository interface {
-     Create(
-         ctx context.Context,
-         name string,
-         email string,
-     ) (*User, error)
-
-     Update(
-         ctx context.Context,
-         id int,
-         name string,
-         email string,
-     ) error
-
-     Delete(
-         ctx context.Context,
-         id int,
-     ) error
- }
+ type UserRepository interface {
+     Create(ctx context.Context, name string, email string) (*User, error)
+     Update(ctx context.Context, id int, name string, email string) error
+     Delete(ctx context.Context, id int) error
+ }

Variadic Interface Methods:

  type VariadicInterface interface {
-     Process(
-         items ...string,
-     ) error
+     Process(items ...string) error
  }

Handler Interfaces (Functional Parameters):

  type HandlerInterface interface {
-     Handle(
-         ctx context.Context,
-         handler func(string) error,
-     ) error
+     Handle(ctx context.Context, handler func(string) error) error

      HandleMultiple(ctx context.Context, handlers ...func(string) error) error
  }

4. Struct Fields with Function Types

Simple Handlers:

  type Handler struct {
-     Process func(
-         ctx context.Context,
-     ) error
+     Process func(ctx context.Context) error
  }

Multiple Function Fields:

  type MultiHandler struct {
-     OnStart func(
-         id string,
-     ) error
+     OnStart func(id string) error

      OnStop func() error

-     OnProcessWithVeryLongNameAndManyParameters func(
-         parameterOne string,
-         parameterTwo string,
-         parameterThree string,
-         parameterFour string,
-     ) error
+     OnProcessWithVeryLongNameAndManyParameters func(parameterOne string,
+         parameterTwo string, parameterThree string, parameterFour string) error

-     GetData func(
-         key string,
-     ) (string, error)
+     GetData func(key string) (string, error)
  }

Variadic and Named Returns:

  type VariadicHandler struct {
-     Process func(
-         items ...string,
-     ) error
+     Process func(items ...string) error
  }

  type NamedReturnsHandler struct {
-     Process func(
-         id string,
-     ) (result string, err error)
+     Process func(id string) (result string, err error)
  }

Higher-Order Functions:

  type HigherOrderHandler struct {
-     GetHandler func(
-         config string,
-     ) func(string) error
+     GetHandler func(config string) func(string) error
  }

  type CallbackHandler struct {
-     Process func(
-         callback func(string) error,
-     ) error
+     Process func(callback func(string) error) error

      ProcessMultiple func(callback func(string) error, fallback func() error) error
  }

Struct Tags Are Budgeted, Never Rewritten:

  type Registry struct {
-     Handler func(
-         w int,
-         r string,
-     ) error `json:"handler"`
+     Handler func(w int, r string) error `json:"handler"`
  }

The tag stays on the collapsed line, so its width counts against max-line-len: a signature collapses only when signature + tag fit. When the tag is too long for any single line, the field keeps the hand-written split shape (params one per line, ) at the parent indent) and the tag is left exactly where it was — sigfmt never edits tags and never fights gofmt's tag alignment.

5. Generics (Go 1.18+)

Basic Generics:

- func Generic[
-     T any,
- ](
-     val T,
- ) {
+ func Generic[T any](val T) {
      // ...
  }

Multiple Type Parameters:

- func Map[
-     T any,
-     R any,
- ](
-     items []T,
-     fn func(T) R,
- ) []R {
+ func Map[T any, R any](items []T, fn func(T) R) []R {
      result := make([]R, len(items))
      for i, item := range items {
          result[i] = fn(item)
      }
      return result
  }

Generic Interfaces:

  type GenericInterface[T any] interface {
-     Process(
-         item T,
-     ) error
+     Process(item T) error

      GetAll() []T
  }

  type MultiGenericInterface[K comparable, V any] interface {
-     Get(
-         key K,
-     ) (V, bool)
+     Get(key K) (V, bool)

-     Set(
-         key K,
-         value V,
-     ) error
+     Set(key K, value V) error

      Delete(key K)
  }

Generic Struct Fields:

  type GenericHandler[T any] struct {
-     Process func(
-         item T,
-     ) error
+     Process func(item T) error

      Transform func(item T) T
  }

6. Complex Type Definitions

Channel Types:

- func Stream(
-     ctx context.Context,
-     in <-chan Item,
-     out chan<- Result,
- ) error {
+ func Stream(ctx context.Context, in <-chan Item, out chan<- Result) error {
      // ...
  }

API Handlers:

- func CreateUser(
-     w http.ResponseWriter,
-     r *http.Request,
- ) {
+ func CreateUser(w http.ResponseWriter, r *http.Request) {
      // ...
  }

- func UpdateUser(w http.ResponseWriter, r *http.Request, id string,
-     name string, email string) {
+ func UpdateUser(w http.ResponseWriter, r *http.Request, id string, name string, email string) {
      // ...
  }

Complex Order Processing:

- func ProcessOrder(
-     ctx context.Context,
-     orderID string,
-     items []Item,
-     shippingAddress *Address,
-     paymentMethod PaymentMethod,
-     options ...Option,
- ) (
-     *Order,
-     error,
- ) {
+ func ProcessOrder(ctx context.Context, orderID string, items []Item, shippingAddress *Address, paymentMethod PaymentMethod, options ...Option) (*Order, error) {
      // ...
  }

7. Parameter Grouping (Advanced Feature)

When using param-groups configuration, related parameters are kept together for better semantic organization:

*Database Operations (context.Context + sql.Tx grouping):

  // Configuration: param-groups: [["context.Context", "*sql.Tx"]]

- func LongQueryFunctionWithManyArguments(
-     ctx context.Context,
-     tx *sql.Tx,
-     query string,
-     args ...interface{},
- ) error {
+ func LongQueryFunctionWithManyArguments(
+     ctx context.Context, tx *sql.Tx,
+     query string, args ...interface{}) error {
      return nil
  }

Repository Interface with Grouping:

  type Repository interface {
-     Create(
-         ctx context.Context,
-         tx *sql.Tx,
-         id int,
-         name string,
-     ) error
+     Create(ctx context.Context, tx *sql.Tx,
+         id int, name string) error

-     Update(
-         ctx context.Context,
-         data []byte,
-     ) error
+     Update(ctx context.Context, data []byte) error
  }

Handler Functions with Grouping:

  type Handler struct {
-     OnCreate func(
-         ctx context.Context,
-         tx *sql.Tx,
-         data string,
-     ) error
+     OnCreate func(ctx context.Context,
+         tx *sql.Tx, data string) error

-     OnUpdate func(
-         ctx context.Context,
-         id int,
-     ) error
+     OnUpdate func(ctx context.Context, id int) error
  }

8. Long Signatures (Reformatting Strategy)

When signatures don't fit on one line, sigfmt applies intelligent packing to maximize readability while minimizing vertical space:

  func ComplexCalculation(
-     inputMatrix [][]float64,
-     weights []float64,
-     bias float64,
-     activationFunc func(float64) float64,
-     learningRate float64,
-     epochs int,
-     dropoutRate float64,
  ) error {
+ func ComplexCalculation(
+     inputMatrix [][]float64, weights []float64, bias float64,
+     activationFunc func(float64) float64, learningRate float64,
+     epochs int, dropoutRate float64,
+ ) error {
      // ...
  }

9. Local Structs in Functions

  func ComplexCase() {
      type LocalStruct struct {
-         Handler func(
-             ctx context.Context,
-             id string,
-         ) error
+         Handler func(ctx context.Context, id string) error

          Simple func() error

-         VeryLongHandler func(
-             parameterWithVeryLongName string,
-             anotherParameterWithVeryLongName string,
-             yetAnotherParameterWithVeryLongName string,
-         ) error
+         VeryLongHandler func(parameterWithVeryLongName string,
+             anotherParameterWithVeryLongName string,
+             yetAnotherParameterWithVeryLongName string) error
      }
      _ = LocalStruct{}
  }

⚖️ Comparison: sigfmt vs gofumpt vs golines vs wsl

sigfmt is a focused tool — it formats function signatures and nothing else. The other tools cover broader or adjacent domains. Use this matrix to decide when to reach for each.

Feature sigfmt gofumpt golines wsl
Domain Function signatures only General Go formatting (strict gofmt) Long-line shortening (any code) Whitespace / cuddling rules
Collapse multi-line sigs that fit ✅ ❌ ❌ partial
Pack multiple params per line ✅ ❌ ❌ ❌
Semantic parameter grouping (param-groups) ✅ ❌ ❌ ❌
Aggressive interface/struct packing ✅ ❌ ❌ ❌
Configurable line length (max-line-len) ✅ ❌ ✅ ❌
Shortens non-signature long lines ❌ ❌ ✅ ❌
Enforces block cuddling / blank lines ❌ ❌ ❌ ✅
golangci-lint plugin with --fix ✅ ✅ ✅ ✅
Suggested fixes (diagnostics + autofix) ✅ ✅ ✅ (rewrites files) ✅

When to use sigfmt

  • You want consistent function signatures — collapsed when they fit, semantically packed when they don't.
  • You have interface-heavy or struct-with-callbacks code and want to minimize vertical bloat.
  • You need semantic grouping (e.g. context.Context always paired with a transaction handle).

When to combine sigfmt with other tools

  • sigfmt + gofumpt: gofumpt for general formatting, sigfmt for signatures. They don't conflict.
  • sigfmt + golines: golines to shorten non-signature long lines (struct literals, call chains), sigfmt to own signatures.
  • sigfmt + wsl: wsl for block cuddling and blank-line rules, sigfmt for signature packing. wsl's single-line/multi-line heuristic for signatures is coarser than sigfmt's packing strategy — let sigfmt own that domain.

Bottom line: sigfmt is complementary, not a replacement. It fills the gap that gofumpt, golines, and wsl leave open: intelligent, semantic formatting of Go function signatures. For full migration recipes and side-by-side examples, see the Configuration Cookbook.

⚡ Performance

  • Fast: Does not require type loading (register.LoadModeSyntax). AST-only analysis, no type information.
  • Parallel: Supports parallel execution via golangci-lint.

Benchmark suite

go test -bench . -benchmem (or make bench) measures the analyzer through its public API on a deterministic generated corpus — 12 files × 40 signatures, cycling collapsible / packable / interface / struct-field shapes. Every corpus file is guarded to be a gofmt fixed point, so runs are comparable.

Measured numbers, Go 1.25 / linux-arm64, medians of 3 × 5s runs:

Benchmark Profile Time/op Allocs/op
BenchmarkAnalyzer violations ~7.2 ms ~53.8 k
BenchmarkAnalyzer clean ~3.6 ms ~25.5 k
BenchmarkSigfmtWithParse violations ~10.2 ms ~84 k
BenchmarkSigfmtWithParse clean ~5.6 ms ~48.6 k
BenchmarkGofmtBaseline (gofmt) violations ~3.4 ms ~29.8 k
BenchmarkGofmtBaseline (gofmt) clean ~2.5 ms ~24.3 k

Reading the numbers honestly: including parsing, sigfmt is currently ~3.0× gofmt on unformatted code and ~2.2× on already-clean code — above the original <2× aspiration. In absolute terms the analyzer costs ~15µs per signature (7.2 ms for 480 signatures), which is negligible next to package loading in a real golangci-lint run. CPU profiles attribute the cost to allocation pressure (53.8 k allocs/op in the analysis loop), so the path to the <2× target is allocation reduction in extraction/rendering — tracked as follow-up optimization work, not hidden from the numbers.

🤔 FAQ

**Why not use gofmt or gofumpt?** They provide basic formatting but lack strict rules for line length and parameter packing. sigfmt` complements them.

Can parameter packing be disabled? Yes, use pack-struct-fields: false and pack-interface-methods: false.

How to handle special cases? Use //nolint:sigfmt to ignore specific functions.

💻 Development

The project uses Makefile for common development tasks:

  • make test: Run all tests.
  • make test-race: Run tests with the race detector.
  • make test-coverage: Generate a test coverage report.
  • make test-update-golden: Update golden files (use when diagnostic messages or expected fixes change).
  • make fuzz: Fuzz the analyzer for 60s over hostile signatures (see below).
  • make fmt: Format Go code.
  • make lint: Run linters.
  • make check: Run all checks (test, fmt, lint).
  • make build-example: Build the example custom golangci-lint binary.
  • make run-example: Run the linter on the example project.
  • make clean: Clean up build artifacts.

Fuzzing

The analyzer ships a native Go fuzzing harness, FuzzFixInvariants (fuzz_test.go). It mutates hostile signatures and asserts the three invariants the glitch corpus pins, on every input that parses:

  1. the applied fix output still parses,
  2. gofmt-clean input stays gofmt-clean (a signature formatter must never break formatting — making unformatted files formatted is gofmt's job),
  3. a second pass proposes nothing (idempotence).
make fuzz                                # 60-second campaign
go test -fuzz=FuzzFixInvariants -fuzztime=10m   # longer campaign
go test -run=FuzzFixInvariants           # replay seeds + committed crashers

Inputs that make an invariant fail are written to testdata/fuzz/FuzzFixInvariants/ and must be committed together with the fix — they become permanent regression cases. The seed corpus is derived from the glitch corpus (extreme parameter counts, nested generics, pointer-of-pointer types, variadic func chains).

Updating Diagnostic Messages

When updating diagnostic messages or changing the expected output of the linter, follow these steps:

  1. Update constants in linter.go (or internal/format/formatter.go for messages).
  2. Run tests (make test). They will likely fail, indicating where the messages need to be updated.
  3. Update diagnostic messages in test files: find testdata/src -name "*.go" -exec sed -i '' 's/OLD_MESSAGE/NEW_MESSAGE/g' {} \; (replace OLD_MESSAGE and NEW_MESSAGE accordingly).
  4. Update golden files: make test-update-golden
  5. Verify all tests pass: make test

❤️ Community & Acknowledgements

This project is made with ❤️ for the open-source community by Vadim Fedorenko.

🤝 Contributing

Contributions are welcome!

  1. Fork the repository.
  2. Create a branch.
  3. Ensure tests pass (make test).
  4. Submit a Pull Request.

📝 License

MIT License

🔗 Useful Links

About

golangci-lint plugin (Go 1.25+) that checks and auto-reformats function signatures — declarations, methods, literals, interfaces, struct fields — into the most compact form that fits your line budget. Struct-tag aware.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages