A source rewriter for Bow that migrates code to fork syntax: T! result types, expr! error propagation, nilable pointer types (*T / *T?), and struct/interface shorthand (struct T { … }, interface I { … }).
It walks a module or directory of Go files and rewrites them in place. Files are processed per package so nilable-pointer inference can use call sites within the package.
See docs/examples.md for a full before/after catalog, including what reference rewrites are not performed.
See docs/config.md to enable or disable individual rewrite passes via modernize.json.
Adds nilable_pointers warnings to go.mod when missing, then annotates pointer types:
*T?when nil can flow in (nil assignment,return nil,nilarguments, zero-initvar p *T,json:",omitempty"pointer fields)*T(strict) when no nil evidence is found in the package — preferred wherever inference allows
Respects //go:nilable_pointers disable … end regions (those types are left unchanged).
Optional ?? zero fallback for nilable slice fields (nil_coalesce_fallback, default off):
consume(key.Plaintext ?? []byte{})Set "nil_coalesce_fallback": true in modernize.json to enable; when off, modernize prints a notice with this example.
Example:
func Find(id int) *User {
return nil
}
func Conn() *DB {
if db == nil {
panic("uninitialized")
}
return db
}becomes:
func Find(id int) *User? {
return nil
}
func Conn() *DB {
if db == nil {
panic("uninitialized")
}
return db
}(db would be *DB? if it is zero-initialized or assigned nil elsewhere.)
Drop redundant nil on success (in T! functions):
return value, nil → return valuePropagate errors with ! (in error or T! functions, when err is not used again in the block):
if err := fn(); err != nil {
return err
}→
fn()!Skips vendor/, .git/, testdata/, and _test.go files.
Rewrites named struct and interface type declarations:
type Person struct { Name string } → struct Person { Name string }
type Stringer interface { String() string } → interface Stringer { String() string }Skips parenthesized type ( … ) groups, type aliases, generics, and non-struct/interface types. Also rewrites local type declarations inside function bodies.
Removes unreachable if recv == nil { … } guards in pointer-receiver methods (always unreachable in Bow — see nil receivers). Adds ?. on call chains only when the callee had such a guard (equivalent short-circuit), never merely because a return type is nullable.
fmt.Errorf → errors.New when the format string is a literal with no %w (no error chaining):
return fmt.Errorf("something failed") → return errors.New("something failed")
return fmt.Errorf("bad %s", name) → return errors.New("bad %s", name)
return fmt.Errorf("wrap: %w", err) → unchangedCustom error types that define Error() string get an errors.Base embed:
- Message-only types (single
msg/messagefield,Error()returns it): field andError()are removed; constructions becomeerrors.NewCustom[YourError](...). - Types with extra domain fields:
errors.Baseis added; constructor returns are rewritten to callerrors.InitCustom(&e.Base, "%s", e.Error()).
type AppError struct { msg string }
func (e AppError) Error() string { return e.msg }
func fail() error { return AppError{msg: "oops"} }→
type AppError struct { errors.Base }
func fail() error { return errors.NewCustom[AppError]("oops") }Build and run with Bow as GOROOT — the output uses T!, expr!, and *T?, which standard Go does not accept.
export GOROOT=/path/to/go-fork
export PATH=$GOROOT/bin:$PATH
go build -o modernize .
# default root is "."; pass a path to scan another tree
./modernize ./path/to/module
# optional: per-target config at ./path/to/module/modernize.json
# or MODERNIZE_CONFIG=/path/to/modernize.jsonWith step_commits enabled (default), each rewrite pass is applied and committed separately when the target is a git or hg repository and the matching git or hg command is on PATH. See docs/config.md.
Each modified file path is printed; a summary count is written to stderr.
MIT — see LICENSE.