diff options
| author | Taras Madan <tarasmadan@google.com> | 2023-12-05 15:10:03 +0100 |
|---|---|---|
| committer | Taras Madan <tarasmadan@google.com> | 2023-12-06 11:31:44 +0000 |
| commit | 2ab72b4feef2c97f22f90cfbf9e45a6cfcd08bda (patch) | |
| tree | a6d19b94b6399fcc00a6cfa430885cd349dd1533 /vendor/github.com/ghostiam | |
| parent | e08e8f492d31d672cc245944c185f8aadf2ee695 (diff) | |
vendor: updates
Diffstat (limited to 'vendor/github.com/ghostiam')
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/.goreleaser.yaml | 24 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/Makefile | 9 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/README.md | 73 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/posfilter.go | 65 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/processor.go | 234 | ||||
| -rw-r--r-- | vendor/github.com/ghostiam/protogetter/protogetter.go | 183 |
7 files changed, 609 insertions, 0 deletions
diff --git a/vendor/github.com/ghostiam/protogetter/.goreleaser.yaml b/vendor/github.com/ghostiam/protogetter/.goreleaser.yaml new file mode 100644 index 000000000..a70d0fb00 --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/.goreleaser.yaml @@ -0,0 +1,24 @@ +before: + hooks: + - go mod tidy +builds: + - id: protogetter + main: ./cmd/protogetter + binary: protogetter + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^ci:'
\ No newline at end of file diff --git a/vendor/github.com/ghostiam/protogetter/LICENSE b/vendor/github.com/ghostiam/protogetter/LICENSE new file mode 100644 index 000000000..b4449661b --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Vladislav Fursov (GhostIAm) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
\ No newline at end of file diff --git a/vendor/github.com/ghostiam/protogetter/Makefile b/vendor/github.com/ghostiam/protogetter/Makefile new file mode 100644 index 000000000..af4b62bdf --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/Makefile @@ -0,0 +1,9 @@ +.PHONY: test +test: + cd testdata && make vendor + go test -v ./... + +.PHONY: install +install: + go install ./cmd/protogetter + @echo "Installed in $(shell which protogetter)" diff --git a/vendor/github.com/ghostiam/protogetter/README.md b/vendor/github.com/ghostiam/protogetter/README.md new file mode 100644 index 000000000..c033e9597 --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/README.md @@ -0,0 +1,73 @@ +# Protogetter +Welcome to the Protogetter project! + +## Overview +Protogetter is a linter developed specifically for Go programmers working with nested `protobuf` types.\ +It's designed to aid developers in preventing `invalid memory address or nil pointer dereference` errors arising from direct access of nested `protobuf` fields. + +When working with `protobuf`, it's quite common to have complex structures where a message field is contained within another message, which itself can be part of another message, and so on. +If these fields are accessed directly and some field in the call chain will not be initialized, it can result in application panic. + +Protogetter addresses this issue by suggesting use of getter methods for field access. + +## How does it work? +Protogetter analyzes your Go code and helps detect direct `protobuf` field accesses that could give rise to panic.\ +The linter suggests using getters: +```go +m.GetFoo().GetBar().GetBaz() +``` +instead of direct field access: +```go +m.Foo.Bar.Baz +``` + +And you will then only need to perform a nil check after the final call: +```go +if m.GetFoo().GetBar().GetBaz() != nil { + // Do something with m.GetFoo().GetBar().GetBaz() +} +``` +instead of: +```go +if m.Foo != nil { + if m.Foo.Bar != nil { + if m.Foo.Bar.Baz != nil { + // Do something with m.Foo.Bar.Baz + } + } +} +``` + +or use zero values: + +```go +// If one of the methods returns `nil` we will receive 0 instead of panic. +v := m.GetFoo().GetBar().GetBaz().GetInt() +``` + +instead of panic: + +```go +// If at least one structure in the chains is not initialized, we will get a panic. +v := m.Foo.Bar.Baz.Int +``` + +which simplifies the code and makes it more reliable. + +## Installation + +```bash +go install github.com/ghostiam/protogetter/cmd/protogetter@latest +``` + +## Usage + +To run the linter: +```bash +protogetter ./... +``` + +Or to apply suggested fixes directly: +```bash +protogetter --fix ./... +``` diff --git a/vendor/github.com/ghostiam/protogetter/posfilter.go b/vendor/github.com/ghostiam/protogetter/posfilter.go new file mode 100644 index 000000000..82075ccb1 --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/posfilter.go @@ -0,0 +1,65 @@ +package protogetter + +import ( + "go/token" +) + +type PosFilter struct { + positions map[token.Pos]struct{} + alreadyReplaced map[string]map[int][2]int // map[filename][line][start, end] +} + +func NewPosFilter() *PosFilter { + return &PosFilter{ + positions: make(map[token.Pos]struct{}), + alreadyReplaced: make(map[string]map[int][2]int), + } +} + +func (f *PosFilter) IsFiltered(pos token.Pos) bool { + _, ok := f.positions[pos] + return ok +} + +func (f *PosFilter) AddPos(pos token.Pos) { + f.positions[pos] = struct{}{} +} + +func (f *PosFilter) IsAlreadyReplaced(fset *token.FileSet, pos, end token.Pos) bool { + filePos := fset.Position(pos) + fileEnd := fset.Position(end) + + lines, ok := f.alreadyReplaced[filePos.Filename] + if !ok { + return false + } + + lineRange, ok := lines[filePos.Line] + if !ok { + return false + } + + if lineRange[0] <= filePos.Offset && fileEnd.Offset <= lineRange[1] { + return true + } + + return false +} + +func (f *PosFilter) AddAlreadyReplaced(fset *token.FileSet, pos, end token.Pos) { + filePos := fset.Position(pos) + fileEnd := fset.Position(end) + + lines, ok := f.alreadyReplaced[filePos.Filename] + if !ok { + lines = make(map[int][2]int) + f.alreadyReplaced[filePos.Filename] = lines + } + + lineRange, ok := lines[filePos.Line] + if ok && lineRange[0] <= filePos.Offset && fileEnd.Offset <= lineRange[1] { + return + } + + lines[filePos.Line] = [2]int{filePos.Offset, fileEnd.Offset} +} diff --git a/vendor/github.com/ghostiam/protogetter/processor.go b/vendor/github.com/ghostiam/protogetter/processor.go new file mode 100644 index 000000000..445f136b8 --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/processor.go @@ -0,0 +1,234 @@ +package protogetter + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "strings" +) + +type processor struct { + info *types.Info + filter *PosFilter + + to strings.Builder + from strings.Builder + err error +} + +func Process(info *types.Info, filter *PosFilter, n ast.Node) (*Result, error) { + p := &processor{ + info: info, + filter: filter, + } + + return p.process(n) +} + +func (c *processor) process(n ast.Node) (*Result, error) { + switch x := n.(type) { + case *ast.AssignStmt: + // Skip any assignment to the field. + for _, lhs := range x.Lhs { + c.filter.AddPos(lhs.Pos()) + } + + case *ast.IncDecStmt: + // Skip any increment/decrement to the field. + c.filter.AddPos(x.X.Pos()) + + case *ast.UnaryExpr: + if x.Op == token.AND { + // Skip all expressions when the field is used as a pointer. + // Because this is not direct reading, but most likely writing by pointer (for example like sql.Scan). + c.filter.AddPos(x.X.Pos()) + } + + case *ast.CallExpr: + f, ok := x.Fun.(*ast.SelectorExpr) + if !ok { + return &Result{}, nil + } + + if !isProtoMessage(c.info, f.X) { + return &Result{}, nil + } + + c.processInner(x) + + case *ast.SelectorExpr: + if !isProtoMessage(c.info, x.X) { + // If the selector is not on a proto message, skip it. + return &Result{}, nil + } + + c.processInner(x) + + default: + return nil, fmt.Errorf("not implemented for type: %s (%s)", reflect.TypeOf(x), formatNode(n)) + } + + if c.err != nil { + return nil, c.err + } + + return &Result{ + From: c.from.String(), + To: c.to.String(), + }, nil +} + +func (c *processor) processInner(expr ast.Expr) { + switch x := expr.(type) { + case *ast.Ident: + c.write(x.Name) + + case *ast.BasicLit: + c.write(x.Value) + + case *ast.UnaryExpr: + if x.Op == token.AND { + c.write(formatNode(x)) + return + } + + c.write(x.Op.String()) + c.processInner(x.X) + + case *ast.SelectorExpr: + c.processInner(x.X) + c.write(".") + + // If getter exists, use it. + if methodIsExists(c.info, x.X, "Get"+x.Sel.Name) { + c.writeFrom(x.Sel.Name) + c.writeTo("Get" + x.Sel.Name + "()") + return + } + + // If the selector is not a proto-message or the method has already been called, we leave it unchanged. + // This approach is significantly more efficient than verifying the presence of methods in all cases. + c.write(x.Sel.Name) + + case *ast.CallExpr: + c.processInner(x.Fun) + c.write("(") + for i, arg := range x.Args { + if i > 0 { + c.write(",") + } + c.processInner(arg) + } + c.write(")") + + case *ast.IndexExpr: + c.processInner(x.X) + c.write("[") + c.processInner(x.Index) + c.write("]") + + case *ast.BinaryExpr: + c.processInner(x.X) + c.write(x.Op.String()) + c.processInner(x.Y) + + case *ast.ParenExpr: + c.write("(") + c.processInner(x.X) + c.write(")") + + case *ast.StarExpr: + c.write("*") + c.processInner(x.X) + + case *ast.CompositeLit: + c.write(formatNode(x)) + + case *ast.TypeAssertExpr: + c.write(formatNode(x)) + + default: + c.err = fmt.Errorf("processInner: not implemented for type: %s", reflect.TypeOf(x)) + } +} + +func (c *processor) write(s string) { + c.writeTo(s) + c.writeFrom(s) +} + +func (c *processor) writeTo(s string) { + c.to.WriteString(s) +} + +func (c *processor) writeFrom(s string) { + c.from.WriteString(s) +} + +// Result contains source code (from) and suggested change (to) +type Result struct { + From string + To string +} + +func (r *Result) Skipped() bool { + // If from and to are the same, skip it. + return r.From == r.To +} + +func isProtoMessage(info *types.Info, expr ast.Expr) bool { + // First, we are checking for the presence of the ProtoReflect method which is currently being generated + // and corresponds to v2 version. + // https://pkg.go.dev/google.golang.org/protobuf@v1.31.0/proto#Message + const protoV2Method = "ProtoReflect" + ok := methodIsExists(info, expr, protoV2Method) + if ok { + return true + } + + // Afterwards, we are checking the ProtoMessage method. All the structures that implement the proto.Message interface + // have a ProtoMessage method and are proto-structures. This interface has been generated since version 1.0.0 and + // continues to exist for compatibility. + // https://pkg.go.dev/github.com/golang/protobuf/proto?utm_source=godoc#Message + const protoV1Method = "ProtoMessage" + ok = methodIsExists(info, expr, protoV1Method) + if ok { + // Since there is a protoc-gen-gogo generator that implements the proto.Message interface, but may not generate + // getters or generate from without checking for nil, so even if getters exist, we skip them. + const protocGenGoGoMethod = "MarshalToSizedBuffer" + return !methodIsExists(info, expr, protocGenGoGoMethod) + } + + return false +} + +func methodIsExists(info *types.Info, x ast.Expr, name string) bool { + if info == nil { + return false + } + + t := info.TypeOf(x) + if t == nil { + return false + } + + ptr, ok := t.Underlying().(*types.Pointer) + if ok { + t = ptr.Elem() + } + + named, ok := t.(*types.Named) + if !ok { + return false + } + + for i := 0; i < named.NumMethods(); i++ { + if named.Method(i).Name() == name { + return true + } + } + + return false +} diff --git a/vendor/github.com/ghostiam/protogetter/protogetter.go b/vendor/github.com/ghostiam/protogetter/protogetter.go new file mode 100644 index 000000000..80a829672 --- /dev/null +++ b/vendor/github.com/ghostiam/protogetter/protogetter.go @@ -0,0 +1,183 @@ +package protogetter + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "log" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" +) + +type Mode int + +const ( + StandaloneMode Mode = iota + GolangciLintMode +) + +const msgFormat = "avoid direct access to proto field %s, use %s instead" + +func NewAnalyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "protogetter", + Doc: "Reports direct reads from proto message fields when getters should be used", + Run: func(pass *analysis.Pass) (any, error) { + Run(pass, StandaloneMode) + return nil, nil + }, + } +} + +func Run(pass *analysis.Pass, mode Mode) []Issue { + nodeTypes := []ast.Node{ + (*ast.AssignStmt)(nil), + (*ast.CallExpr)(nil), + (*ast.SelectorExpr)(nil), + (*ast.IncDecStmt)(nil), + (*ast.UnaryExpr)(nil), + } + + // Skip protoc-generated files. + var files []*ast.File + for _, f := range pass.Files { + if !isProtocGeneratedFile(f) { + files = append(files, f) + + // ast.Print(pass.Fset, f) + } + } + + ins := inspector.New(files) + + var issues []Issue + + filter := NewPosFilter() + ins.Preorder(nodeTypes, func(node ast.Node) { + report := analyse(pass, filter, node) + if report == nil { + return + } + + switch mode { + case StandaloneMode: + pass.Report(report.ToDiagReport()) + case GolangciLintMode: + issues = append(issues, report.ToIssue(pass.Fset)) + } + }) + + return issues +} + +func analyse(pass *analysis.Pass, filter *PosFilter, n ast.Node) *Report { + // fmt.Printf("\n>>> check: %s\n", formatNode(n)) + // ast.Print(pass.Fset, n) + if filter.IsFiltered(n.Pos()) { + // fmt.Printf(">>> filtered\n") + return nil + } + + result, err := Process(pass.TypesInfo, filter, n) + if err != nil { + pass.Report(analysis.Diagnostic{ + Pos: n.Pos(), + End: n.End(), + Message: fmt.Sprintf("error: %v", err), + }) + + return nil + } + + // If existing in filter, skip it. + if filter.IsFiltered(n.Pos()) { + return nil + } + + if result.Skipped() { + return nil + } + + // If the expression has already been replaced, skip it. + if filter.IsAlreadyReplaced(pass.Fset, n.Pos(), n.End()) { + return nil + } + // Add the expression to the filter. + filter.AddAlreadyReplaced(pass.Fset, n.Pos(), n.End()) + + return &Report{ + node: n, + result: result, + } +} + +// Issue is used to integrate with golangci-lint's inline auto fix. +type Issue struct { + Pos token.Position + Message string + InlineFix InlineFix +} + +type InlineFix struct { + StartCol int // zero-based + Length int + NewString string +} + +type Report struct { + node ast.Node + result *Result +} + +func (r *Report) ToDiagReport() analysis.Diagnostic { + msg := fmt.Sprintf(msgFormat, r.result.From, r.result.To) + + return analysis.Diagnostic{ + Pos: r.node.Pos(), + End: r.node.End(), + Message: msg, + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: msg, + TextEdits: []analysis.TextEdit{ + { + Pos: r.node.Pos(), + End: r.node.End(), + NewText: []byte(r.result.To), + }, + }, + }, + }, + } +} + +func (r *Report) ToIssue(fset *token.FileSet) Issue { + msg := fmt.Sprintf(msgFormat, r.result.From, r.result.To) + return Issue{ + Pos: fset.Position(r.node.Pos()), + Message: msg, + InlineFix: InlineFix{ + StartCol: fset.Position(r.node.Pos()).Column - 1, + Length: len(r.result.From), + NewString: r.result.To, + }, + } +} + +func isProtocGeneratedFile(f *ast.File) bool { + return len(f.Comments) > 0 && strings.HasPrefix(f.Comments[0].Text(), "Code generated by protoc-gen-go") +} + +func formatNode(node ast.Node) string { + buf := new(bytes.Buffer) + if err := format.Node(buf, token.NewFileSet(), node); err != nil { + log.Printf("Error formatting expression: %v", err) + return "" + } + + return buf.String() +} |
