From 6b9630522664bffea8b2fe55632f5bd5c3c67180 Mon Sep 17 00:00:00 2001 From: Lubomir Cvrk Date: Fri, 5 Dec 2025 09:45:24 +0100 Subject: [PATCH 1/3] Add propro linter --- .golangci.next.reference.yml | 16 +++ docs/data/linters_info.json | 9 ++ docs/data/thanks.json | 8 ++ go.mod | 1 + go.sum | 2 + jsonschema/golangci.next.jsonschema.json | 22 ++++ pkg/config/linters_settings.go | 6 + pkg/golinters/propro/propro.go | 20 +++ .../propro/propro_integration_test.go | 11 ++ .../propro/testdata/entities/entities.go | 5 + pkg/golinters/propro/testdata/propro.go | 117 ++++++++++++++++++ pkg/golinters/propro/testdata/propro.yml | 8 ++ pkg/golinters/propro/testdata/propro_cgo.go | 35 ++++++ pkg/lint/lintersdb/builder_linter.go | 6 + 14 files changed, 266 insertions(+) create mode 100644 pkg/golinters/propro/propro.go create mode 100644 pkg/golinters/propro/propro_integration_test.go create mode 100644 pkg/golinters/propro/testdata/entities/entities.go create mode 100644 pkg/golinters/propro/testdata/propro.go create mode 100644 pkg/golinters/propro/testdata/propro.yml create mode 100644 pkg/golinters/propro/testdata/propro_cgo.go diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 2c8bcbf76949..da80593aaf63 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -103,6 +103,7 @@ linters: - prealloc - predeclared - promlinter + - propro - protogetter - reassign - recvcheck @@ -218,6 +219,7 @@ linters: - prealloc - predeclared - promlinter + - propro - protogetter - reassign - recvcheck @@ -2322,6 +2324,20 @@ linters: # UnitAbbreviations detects abbreviated units in the metric name. - UnitAbbreviations + propro: + # Optional: Path to the file that contains the list of entities to be checked. Entities are expected to be specified in + # var EntityList []any = { &module.Entity1{}, &module.Entity2{}, ... } format. + # Default: "" + entity-list-file: "/your/project/path/to/entities.go" + + # Optional: List of structs in the project to check. + # Default: [] + structs: + - StructName1 + - StructName2 + # If no configuration section for propro is provided, it will check all structs in the project. + + protogetter: # Skip files generated by specified generators from the checking. # Checks only the file's initial comment, which must follow the format: "// Code generated by ". diff --git a/docs/data/linters_info.json b/docs/data/linters_info.json index cfa0bfcdb467..40bd9ac6a6c6 100644 --- a/docs/data/linters_info.json +++ b/docs/data/linters_info.json @@ -764,6 +764,15 @@ "isSlow": false, "since": "v1.40.0" }, + { + "name": "propro", + "desc": "public fields' write protection in structs (entities)", + "loadMode": 384, + "originalURL": "https://github.com/digitalstraw/propro", + "internal": false, + "isSlow": false, + "since": "v2.6.3" + }, { "name": "protogetter", "desc": "Reports direct reads from proto message fields when getters should be used", diff --git a/docs/data/thanks.json b/docs/data/thanks.json index 4e012aa1679e..c55c3226cd9f 100644 --- a/docs/data/thanks.json +++ b/docs/data/thanks.json @@ -201,6 +201,14 @@ "profile": "https://github.com/sponsors/denis-tingaikin", "avatar": "https://github.com/denis-tingaikin.png" }, + { + "name": "digitalstraw", + "linters": [ + "propro" + ], + "profile": "https://github.com/sponsors/digitalstraw", + "avatar": "https://github.com/digitalstraw.png" + }, { "name": "dixonwille", "linters": [ diff --git a/go.mod b/go.mod index 158157f734a7..7f6266a8fa69 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/curioswitch/go-reassign v0.3.0 github.com/daixiang0/gci v0.13.7 github.com/denis-tingaikin/go-header v0.5.0 + github.com/digitalstraw/propro v1.0.0 github.com/fatih/color v1.18.0 github.com/firefart/nonamedreturns v1.0.6 github.com/fzipp/gocyclo v0.6.0 diff --git a/go.sum b/go.sum index c0f26984411e..281285f4cf9e 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/digitalstraw/propro v1.0.0 h1:xLrElmkX6zB0cVQea2QgTWrRjbbzLSeU6p9SBIPnChI= +github.com/digitalstraw/propro v1.0.0/go.mod h1:+71vzW8yjWE3EW8NEWRmm8GvjPanHrxqS4OQehP5gfU= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index accf61f97455..bf8656a5ed09 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -880,6 +880,7 @@ "prealloc", "predeclared", "promlinter", + "propro", "protogetter", "reassign", "recvcheck", @@ -3089,6 +3090,24 @@ } } }, + "proproSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "entity-list-file": { + "description": "Path to the Go source file defining the list of protected structs", + "type": "string", + "default": "" + }, + "structs": { + "description": "List of struct names to protect", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "protogetterSettings": { "type": "object", "additionalProperties": false, @@ -4837,6 +4856,9 @@ "promlinter": { "$ref": "#/definitions/settings/definitions/promlinterSettings" }, + "propro": { + "$ref": "#/definitions/settings/definitions/proproSettings" + }, "protogetter": { "$ref": "#/definitions/settings/definitions/protogetterSettings" }, diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index fefa94ca397b..1e09cabf296e 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -283,6 +283,7 @@ type LintersSettings struct { Prealloc PreallocSettings `mapstructure:"prealloc"` Predeclared PredeclaredSettings `mapstructure:"predeclared"` Promlinter PromlinterSettings `mapstructure:"promlinter"` + ProPro ProProSettings `mapstructure:"propro"` ProtoGetter ProtoGetterSettings `mapstructure:"protogetter"` Reassign ReassignSettings `mapstructure:"reassign"` Recvcheck RecvcheckSettings `mapstructure:"recvcheck"` @@ -817,6 +818,11 @@ type PromlinterSettings struct { DisabledLinters []string `mapstructure:"disabled-linters"` } +type ProProSettings struct { + EntityListFile string `mapstructure:"entity-list-file"` + Structs []string `mapstructure:"structs"` +} + type ProtoGetterSettings struct { SkipGeneratedBy []string `mapstructure:"skip-generated-by"` SkipFiles []string `mapstructure:"skip-files"` diff --git a/pkg/golinters/propro/propro.go b/pkg/golinters/propro/propro.go new file mode 100644 index 000000000000..bf430a40f0ec --- /dev/null +++ b/pkg/golinters/propro/propro.go @@ -0,0 +1,20 @@ +package propro + +import ( + "github.com/digitalstraw/propro/pkg/analyzer" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" +) + +func New(settings *config.ProProSettings) *goanalysis.Linter { + cfg := map[string]any{} + + cfg["entityListFile"] = settings.EntityListFile + cfg["structs"] = settings.Structs + + return goanalysis. + NewLinterFromAnalyzer(analyzer.NewAnalyzer(cfg)). + WithLoadMode(goanalysis.LoadModeSyntax). + WithLoadMode(goanalysis.LoadModeTypesInfo) +} diff --git a/pkg/golinters/propro/propro_integration_test.go b/pkg/golinters/propro/propro_integration_test.go new file mode 100644 index 000000000000..fe5c4b222cde --- /dev/null +++ b/pkg/golinters/propro/propro_integration_test.go @@ -0,0 +1,11 @@ +package propro + +import ( + "testing" + + "github.com/golangci/golangci-lint/v2/test/testshared/integration" +) + +func TestFromTestdata(t *testing.T) { + integration.RunTestdata(t) +} diff --git a/pkg/golinters/propro/testdata/entities/entities.go b/pkg/golinters/propro/testdata/entities/entities.go new file mode 100644 index 000000000000..82f22467ae40 --- /dev/null +++ b/pkg/golinters/propro/testdata/entities/entities.go @@ -0,0 +1,5 @@ +package entities + +var EnetityList = []string{ + "Entity", +} diff --git a/pkg/golinters/propro/testdata/propro.go b/pkg/golinters/propro/testdata/propro.go new file mode 100644 index 000000000000..974faff79f10 --- /dev/null +++ b/pkg/golinters/propro/testdata/propro.go @@ -0,0 +1,117 @@ +package testdata + +type UnProtectedEntity struct { + StringField string + IntField int + IntPtrField *int +} + +type Entity struct { + SubEntityViaProperty *SubEntity + ProtectedField string +} + +func (e *Entity) SubEntity() *SubEntity { + return &SubEntity{} +} + +func (e *Entity) SubEntityViaInterface() SubEntityInterface { + return &SubEntity{} +} + +type SubEntityInterface interface { + SetProtectedField(value string) +} + +type SubEntity struct { + ProtectedField string +} + +func (s *SubEntity) SetProtectedField(value string) { + s.ProtectedField = value +} + +type Repository interface { + Read() *Entity +} + +type RepositoryImpl struct{} + +func (r *RepositoryImpl) Read() *Entity { + return &Entity{} +} + +var repo Repository = &RepositoryImpl{} + +func (e *Entity) SetProtectedField(value string) { + e.ProtectedField = value +} + +func SomeFunc1() { + e := &Entity{} + e.SetProtectedField("value") +} + +func SomeFunc2() { + e := &Entity{} + e.ProtectedField = "value" // want "assignment to exported field Entity.ProtectedField is forbidden outside its methods" +} + +func SomeFunc3() { + e := repo.Read() + e.ProtectedField = "value" // want "assignment to exported field Entity.ProtectedField is forbidden outside its methods" +} + +func SomeFunc4() { + e := repo.Read() + e.SetProtectedField("value") +} + +func SomeFunc5() { + e := &Entity{} + sub := e.SubEntity() + sub.SetProtectedField("value") +} +func SomeFunc6() { + e := &Entity{} + sub := e.SubEntity() + sub.ProtectedField = "value" // want "assignment to exported field SubEntity.ProtectedField is forbidden outside its methods" +} + +func SomeFunc7() { + e := &Entity{} + sub := e.SubEntityViaInterface() + sub.SetProtectedField("value") +} + +func SomeFunc8() { + e := &Entity{ + SubEntityViaProperty: &SubEntity{}, + } + e.SubEntityViaProperty.ProtectedField = "value" // want "assignment to exported field SubEntity.ProtectedField is forbidden outside its methods" +} + +func SomeFunc9() { + e := &Entity{ + SubEntityViaProperty: &SubEntity{}, + } + e.SubEntityViaProperty.SetProtectedField("value") + if e.SubEntityViaProperty.ProtectedField != "value" { + } +} + +func SomeFunc10() { + e := &UnProtectedEntity{} + e.StringField = "value" + e.IntField++ + e.IntField-- + e.IntField += 10 + e.IntField -= 10 + e.IntField *= 10 + e.IntField /= 10 + e.IntField = 10 + *(&e.IntField)++ + *(&e.IntField)-- + e.IntPtrField = new(int) + *e.IntPtrField = 20 +} diff --git a/pkg/golinters/propro/testdata/propro.yml b/pkg/golinters/propro/testdata/propro.yml new file mode 100644 index 000000000000..2548d3d75504 --- /dev/null +++ b/pkg/golinters/propro/testdata/propro.yml @@ -0,0 +1,8 @@ +version: "2" + +linters: + settings: + propro: + entity-list-file: "testdata/entities/entities.go" + structs: + - "SubEntity" diff --git a/pkg/golinters/propro/testdata/propro_cgo.go b/pkg/golinters/propro/testdata/propro_cgo.go new file mode 100644 index 000000000000..a77e6c944e2d --- /dev/null +++ b/pkg/golinters/propro/testdata/propro_cgo.go @@ -0,0 +1,35 @@ +package testdata + +/* + #include + #include + + void myprint(char* s) { + printf("%d\n", s); + } +*/ +import "C" + +import ( + "unsafe" +) + +func _() { + cs := C.CString("Hello from stdio\n") + C.myprint(cs) + C.free(unsafe.Pointer(cs)) +} + +type CgoEntity struct { + IntField int +} + +func (s *CgoEntity) SetProtectedField(value int) { + s.IntField = value +} + +func CgoFunc1() { + e := &CgoEntity{} + e.SetProtectedField(1) + e.IntField = 10 // want "assignment to exported field Entity.IntField is forbidden outside its methods" +} diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index 6e9cef1d1b36..88bffe131758 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -91,6 +91,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/prealloc" "github.com/golangci/golangci-lint/v2/pkg/golinters/predeclared" "github.com/golangci/golangci-lint/v2/pkg/golinters/promlinter" + "github.com/golangci/golangci-lint/v2/pkg/golinters/propro" "github.com/golangci/golangci-lint/v2/pkg/golinters/protogetter" "github.com/golangci/golangci-lint/v2/pkg/golinters/reassign" "github.com/golangci/golangci-lint/v2/pkg/golinters/recvcheck" @@ -566,6 +567,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithSince("v1.40.0"). WithURL("https://github.com/yeya24/promlinter"), + linter.NewConfig(propro.New(&cfg.Linters.Settings.ProPro)). + WithSince("v2.8.0"). + WithLoadForGoAnalysis(). + WithURL("https://github.com/digitalstraw/propro"), + linter.NewConfig(protogetter.New(&cfg.Linters.Settings.ProtoGetter)). WithSince("v1.55.0"). WithLoadForGoAnalysis(). From 7142067b554cebcf8535bb83255afe9f0f890a37 Mon Sep 17 00:00:00 2001 From: Lubomir Cvrk Date: Fri, 5 Dec 2025 10:17:05 +0100 Subject: [PATCH 2/3] Add propro linter --- pkg/golinters/propro/testdata/propro.go | 29 ++++++++++----------- pkg/golinters/propro/testdata/propro_cgo.go | 3 ++- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/golinters/propro/testdata/propro.go b/pkg/golinters/propro/testdata/propro.go index 974faff79f10..03d8bb40f558 100644 --- a/pkg/golinters/propro/testdata/propro.go +++ b/pkg/golinters/propro/testdata/propro.go @@ -1,7 +1,7 @@ +//golangcitest:args -Epropro package testdata -type UnProtectedEntity struct { - StringField string +type Entity2 struct { IntField int IntPtrField *int } @@ -101,17 +101,16 @@ func SomeFunc9() { } func SomeFunc10() { - e := &UnProtectedEntity{} - e.StringField = "value" - e.IntField++ - e.IntField-- - e.IntField += 10 - e.IntField -= 10 - e.IntField *= 10 - e.IntField /= 10 - e.IntField = 10 - *(&e.IntField)++ - *(&e.IntField)-- - e.IntPtrField = new(int) - *e.IntPtrField = 20 + e := &Entity2{} + e.IntField++ // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField-- // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField += 10 // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField -= 10 // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField *= 10 // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField /= 10 // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntField = 10 // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + *(&e.IntField)++ // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + *(&e.IntField)-- // want "assignment to exported field Entity2.IntField is forbidden outside its methods" + e.IntPtrField = new(int) // want "assignment to exported field Entity2.IntPtrField is forbidden outside its methods" + *e.IntPtrField = 20 // want "assignment to exported field Entity2.IntPtrField is forbidden outside its methods" } diff --git a/pkg/golinters/propro/testdata/propro_cgo.go b/pkg/golinters/propro/testdata/propro_cgo.go index a77e6c944e2d..76f1d70ef996 100644 --- a/pkg/golinters/propro/testdata/propro_cgo.go +++ b/pkg/golinters/propro/testdata/propro_cgo.go @@ -1,3 +1,4 @@ +//golangcitest:args -Epropro package testdata /* @@ -31,5 +32,5 @@ func (s *CgoEntity) SetProtectedField(value int) { func CgoFunc1() { e := &CgoEntity{} e.SetProtectedField(1) - e.IntField = 10 // want "assignment to exported field Entity.IntField is forbidden outside its methods" + e.IntField = 10 // want "assignment to exported field CgoEntity.IntField is forbidden outside its methods" } From 145ef399807985f654e9aaf57d131306d6e20b87 Mon Sep 17 00:00:00 2001 From: Lubomir Cvrk Date: Sat, 6 Dec 2025 13:29:18 +0100 Subject: [PATCH 3/3] Updated to version 1.2.0, fixed tests. --- go.mod | 2 +- go.sum | 4 ++-- pkg/golinters/propro/testdata/entities/entities.go | 6 ++++-- pkg/golinters/propro/testdata/propro.yml | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 7f6266a8fa69..bd6a9ef23e40 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/curioswitch/go-reassign v0.3.0 github.com/daixiang0/gci v0.13.7 github.com/denis-tingaikin/go-header v0.5.0 - github.com/digitalstraw/propro v1.0.0 + github.com/digitalstraw/propro v1.2.0 github.com/fatih/color v1.18.0 github.com/firefart/nonamedreturns v1.0.6 github.com/fzipp/gocyclo v0.6.0 diff --git a/go.sum b/go.sum index 281285f4cf9e..1cb349955c9f 100644 --- a/go.sum +++ b/go.sum @@ -158,8 +158,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/digitalstraw/propro v1.0.0 h1:xLrElmkX6zB0cVQea2QgTWrRjbbzLSeU6p9SBIPnChI= -github.com/digitalstraw/propro v1.0.0/go.mod h1:+71vzW8yjWE3EW8NEWRmm8GvjPanHrxqS4OQehP5gfU= +github.com/digitalstraw/propro v1.2.0 h1:5b7tjnNQcMKcSVBtiR3IWPEXN3qtnAppoVegA9iN8Ys= +github.com/digitalstraw/propro v1.2.0/go.mod h1:+71vzW8yjWE3EW8NEWRmm8GvjPanHrxqS4OQehP5gfU= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= diff --git a/pkg/golinters/propro/testdata/entities/entities.go b/pkg/golinters/propro/testdata/entities/entities.go index 82f22467ae40..31e1361e69ce 100644 --- a/pkg/golinters/propro/testdata/entities/entities.go +++ b/pkg/golinters/propro/testdata/entities/entities.go @@ -1,5 +1,7 @@ package entities -var EnetityList = []string{ - "Entity", +import "github.com/golangci/golangci-lint/v2/pkg/golinters/propro/testdata" + +var EntityList = []any{ + &testdata.Entity{}, } diff --git a/pkg/golinters/propro/testdata/propro.yml b/pkg/golinters/propro/testdata/propro.yml index 2548d3d75504..0a18d95e2ae3 100644 --- a/pkg/golinters/propro/testdata/propro.yml +++ b/pkg/golinters/propro/testdata/propro.yml @@ -3,6 +3,6 @@ version: "2" linters: settings: propro: - entity-list-file: "testdata/entities/entities.go" + entity-list-file: "pkg/golinters/propro/testdata/entities/entities.go" structs: - - "SubEntity" + - SubEntity