propro stands for Protected Properties. It prevents direct writes to public/exported properties of structs that represent
entities. It is usable especially in DDD (Domain-Driven Design) contexts where entities in the domain layer should enforce business rules
and invariants through methods rather than allowing direct
property manipulation.
Typical solution to this problem before propro was to keep entity properties private (unexported) and create separate model/DTO
structs for ORM, JSON serialization and any other purpose that requires access to exported properties.
ORMs like GORM or tools like JSON encoding require access to exported struct fields. However, when a domain entity is updated, related business logic may need to run and domain events may need to be raised. Bypassing this logic through direct writes to entity properties can compromise domain and data integrity.
To enforce domain state mutation through methods, developers often keep entity properties private and create separate “model” or DTO structs for GORM or JSON with public properties. This approach has several drawbacks: it introduces significant boilerplate (mapping code between entity and model/DTO in both directions + related unit tests), requires maintaining 2 or 3 parallel structures (entity, model and eventually also the DTO for API output), and adds extra unit tests for mapping integrity in both directions (entity -> model, model -> entity).
propro solves this at the linter level by preventing direct writes to exported properties of structs which the developer selected
to be protected by this linter. This allows entities to keep their properties exported and still be used directly with GORM,
JSON and similar tools. Allowing linter-controlled access to entity properties eliminates the need for separate
model/DTO structs and mapping code between them, reducing boilerplate and maintenance overhead.
.golangci.yml:
settings:
propro:
entity-list-files:
- path/to/entity_list.go
- path/to/another_entity_list.go
structs:
- User
- Order-
entity-list-filesmay contain a list of paths to go files each containing anEntityListvariable with the list of empty pointers to the protected structs.- Such a list is required for database migration purposes by ORM tools.
- Example content of such a go file:
var EntityList = []any{ &users.User{}, &users.Order{}, }
- Any listed file may be empty or not present, in which case it contributes no entries. The union of the
EntityListentries across all listed files is used.
-
structs: may contain a list of struct names that should be protected. May be empty or not present.
If both entity-list-files and structs are specified, the union of the two sets is used. If neither is specified,
the linter protects ALL STRUCTS in the analyzed packages. If you don't want any structs to be protected, just disable the linter.
IMPORTANT: this linter is not part of golangci-lint, the PR was declined but discussion is ongoing yet. Feel free to add your comments in the PR.
To use propro with golangci-lint now, you need to build the linter binary from the fork
and place it in your $GOPATH/bin or $PATH.
git clone git@github.com:digitalstraw/golangci-lint.git
cd golangci-lint
git checkout add-propro-linter
go build -o golangci-lint ./cmd/golangci-lint
mv golangci-lint $GOPATH/bin/go get github.com/digitalstraw/propro/v3/git clone git@github.com:digitalstraw/propro.git
go build -o propro cmd/propro/main.go
mv propro $GOPATH/bin/
propro -test=false -entityListFiles=./some/path/entity_config.go,./other/path/entity_config.go -structs=Entity1,Entity2 ./...Available CLI parameters:
-entityListFiles string- comma-separated list of paths to go files each containing anEntityListvariable with the list of protected structs.-structs string- comma-separated list of struct names to be protected.-test bool- whether to run on test files. This flag is provided by the driver, not the analyzer. Default istrueand it is recommended to turn it off.
type Entity struct {
ProtectedField int
}
func (e *Entity) SetProtectedField(value int) {
e.ProtectedField = value
}
func SomeFunc1() {
e := &Entity{}
e.SetProtectedField(10) // OK
a := e.ProtectedField + 10 // OK
e.ProtectedField = 10 // Error
e.ProtectedField += 5 // Error
e.ProtectedField++ // Error
*(&e.ProtectedField)-- // Error
b := &e.ProtectedField // Error
}These edge cases are intentionally not covered by this linter:
- indirect modifications through pointers returned by methods,
- adding pointer to the property to a slice or map,
- passing the property value pointer to the channel,
- using reflection to modify the property,
- using
unsafepackage to modify the property directly, - modifying properties in assembly code.
These unsafe paths exist but are uncommon and intentionally excluded to avoid false positives and excessive complexity.
- Feel free to open pull requests for bug fixes and new features; please include tests.
- Use semantic prefixes for commit messages to determine version bumps:
Fix:for bug fixes -> bumps patch versionNew:for new features -> bumps minor versionBreaking:for breaking changes -> bumps major versionChore:for changes that do not modify src or test files- Anything else will be considered as
Fix: