A simple linter which checks certain visibility rules have been adhered to.
Visibility rules are defined as tags on struct fields. For example:
package main
type MyStruct struct {
Name string `visibility:"readonly"`
Age int `visibility:"readonly"`
}
func main() {
s := MyStruct{}
s.Name = "Frank" // misuse of readonly field: cannot assign
_ = &s.Name // misuse of readonly field: cannot take address
s.Age++ // misuse of readonly field: cannot increment
}
The available rules are as follows:
Checks fields marked as readonly
are only mutated by receiver functions
defined on their containing type.
- Fields stored as a pointer type cannot be checked, instead
vischeck
will emit an error indicating that the field type is invalid. - Structs embedding structs with visibility rules defined can freely modify the embedded fields.
- Struct literals are not yet checked.
In order to simplify (un)marshalling structs, it's often convenient to
export fields. Typically it is safe to read these fields but setting them may
require using a specific API in order to prevent invalid states. Ensuring the
correct APIs are used to set these fields is usually the responsibility of the
developer and code reviewer - this is where vischeck
comes in. With
vischeck
, developers will get a linting error if they attempt to mutate
certain struct fields directly, forcing them to refactor their code to use the
correct API. This linter would be disabled where direct write access to fields
is required (for example in some data access layer).
package models
import (
"errors"
"fmt"
)
type Order struct {
// The visibility tag ensures this field is never manipulated directly,
// forcing developers to use the `SetStatus` method, instead.
Status OrderStatus `visibility:"readonly"`
}
var ErrInvalidOrderStatus = errors.New("invalid status")
func (order *Order) SetStatus(status OrderStatus) error {
if !status.Valid() {
return fmt.Errorf("%w: %s", ErrInvalidOrderStatus, status)
}
if status <= order.Status || status != order.Status+1 {
return fmt.Errorf("%w: can't transition from %s to %s", ErrInvalidOrderStatus, order.Status, status)
}
order.Status = status
return nil
}
type OrderStatus int
const (
OrderStatusInvalid OrderStatus = iota
OrderStatusPending
OrderStatusPaid
OrderStatusShipped
OrderStatusComplete
)
func (status OrderStatus) Valid() bool {
return status > OrderStatusInvalid && status <= OrderStatusComplete
}
func (status OrderStatus) String() string {
return "" // imagine some code here
}
To build vischeck
, simply run make build
. This will produce a standalone
binary (build/vischeck
) and a plugin which can be used with
golangci-lint.
(build/vischeck.so
- an example golangci-lint
configuration is here).