From c07c76b20568c87d6c859bf6b2fc9f7eeeb6b103 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Wed, 27 Mar 2024 19:24:38 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + cmd/foreshadow/main.go | 10 ++++ contrib/example.go | 21 +++++++ go.mod | 7 +++ go.sum | 6 ++ pkg/analyzer/analyzer.go | 105 ++++++++++++++++++++++++++++++++++ pkg/analyzer/analyzer_test.go | 19 ++++++ testdata/src/example.go | 32 +++++++++++ 8 files changed, 201 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/foreshadow/main.go create mode 100644 contrib/example.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/analyzer/analyzer.go create mode 100644 pkg/analyzer/analyzer_test.go create mode 100644 testdata/src/example.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/cmd/foreshadow/main.go b/cmd/foreshadow/main.go new file mode 100644 index 0000000..2ecee9e --- /dev/null +++ b/cmd/foreshadow/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/Crocmagnon/foreshadow/pkg/analyzer" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { + singlechecker.Main(analyzer.Analyzer) +} diff --git a/contrib/example.go b/contrib/example.go new file mode 100644 index 0000000..bdbeed1 --- /dev/null +++ b/contrib/example.go @@ -0,0 +1,21 @@ +package contrib + +import "context" + +func ok() { + ctx := context.Background() + + for i := 0; i < 10; i++ { + ctx := context.WithValue(ctx, "key", i) + _ = ctx + } +} + +func notOk() { + ctx := context.Background() + + for i := 0; i < 10; i++ { + ctx = context.WithValue(ctx, "key", i) + _ = ctx + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7e40c13 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/Crocmagnon/foreshadow + +go 1.22.1 + +require golang.org/x/tools v0.19.0 + +require golang.org/x/mod v0.16.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4672426 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go new file mode 100644 index 0000000..31e800e --- /dev/null +++ b/pkg/analyzer/analyzer.go @@ -0,0 +1,105 @@ +package analyzer + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/printer" + "go/token" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +var Analyzer = &analysis.Analyzer{ + Name: "foreshadow", + Doc: "Enforces context shadowing inside loops.", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +var errUnknown = errors.New("unknown node type") + +func run(pass *analysis.Pass) (interface{}, error) { + inspctr := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.ForStmt)(nil), + (*ast.RangeStmt)(nil), + } + + inspctr.Preorder(nodeFilter, func(node ast.Node) { + body, err := getBody(node) + if err != nil { + return + } + + for _, stmt := range body.List { + assignStmt, ok := stmt.(*ast.AssignStmt) + if !ok { + continue + } + + t := pass.TypesInfo.TypeOf(assignStmt.Lhs[0]) + if t == nil { + continue + } + + if t.String() != "context.Context" { + continue + } + + if assignStmt.Tok == token.DEFINE { + break + } + + assignStmt.Tok = token.DEFINE + suggested := render(pass.Fset, assignStmt) + + pass.Report(analysis.Diagnostic{ + Pos: assignStmt.Pos(), + Message: "context not shadowed in loop", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("replace `=` with `:=`"), + TextEdits: []analysis.TextEdit{ + { + Pos: assignStmt.Pos(), + End: assignStmt.End(), + NewText: []byte(suggested), + }, + }, + }, + }, + }) + + break + } + }) + + return nil, nil +} + +func getBody(node ast.Node) (*ast.BlockStmt, error) { + forStmt, ok := node.(*ast.ForStmt) + if ok { + return forStmt.Body, nil + } + + rangeStmt, ok := node.(*ast.RangeStmt) + if ok { + return rangeStmt.Body, nil + } + + return nil, errUnknown +} + +// render returns the pretty-print of the given node +func render(fset *token.FileSet, x interface{}) string { + var buf bytes.Buffer + if err := printer.Fprint(&buf, fset, x); err != nil { + panic(err) + } + return buf.String() +} diff --git a/pkg/analyzer/analyzer_test.go b/pkg/analyzer/analyzer_test.go new file mode 100644 index 0000000..7a8a228 --- /dev/null +++ b/pkg/analyzer/analyzer_test.go @@ -0,0 +1,19 @@ +package analyzer_test + +import ( + "github.com/Crocmagnon/foreshadow/pkg/analyzer" + "golang.org/x/tools/go/analysis/analysistest" + "os" + "path/filepath" + "testing" +) + +func TestAll(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get wd: %s", err) + } + testdata := filepath.Join(filepath.Dir(filepath.Dir(wd)), "testdata") + + analysistest.Run(t, testdata, analyzer.Analyzer, "./...") +} diff --git a/testdata/src/example.go b/testdata/src/example.go new file mode 100644 index 0000000..d3a8096 --- /dev/null +++ b/testdata/src/example.go @@ -0,0 +1,32 @@ +package src + +import "context" + +func example() { + ctx := context.Background() + + for i := 0; i < 10; i++ { + ctx := context.WithValue(ctx, "key", i) + ctx = context.WithValue(ctx, "other", "val") + } + + for i := 0; i < 10; i++ { + ctx = context.WithValue(ctx, "key", i) // want "context not shadowed in loop" + ctx = context.WithValue(ctx, "other", "val") + } + + for item := range []string{"one", "two", "three"} { + ctx = wrapContext(ctx) // want "context not shadowed in loop" + ctx := context.WithValue(ctx, "key", item) + ctx = wrapContext(ctx) + } + + for { + ctx = wrapContext(ctx) // want "context not shadowed in loop" + break + } +} + +func wrapContext(ctx context.Context) context.Context { + return context.WithoutCancel(ctx) +}