Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

23 changed files with 121 additions and 1202 deletions

View file

@ -1,16 +0,0 @@
# Doc: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
github-actions:
patterns:
- "*" # Group all updates into a single larger pull request.
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"

View file

@ -1,49 +0,0 @@
name: Go
on:
push:
branches: [ "master" ]
tags: [ "*" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
pull-requests: read
jobs:
build:
strategy:
fail-fast: false
matrix:
go: [stable, oldstable]
os: [macos-latest, windows-latest, ubuntu-latest]
name: build
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Build
run: go build -v ./...
- name: Test
run: make test
coverage:
name: coverage
permissions:
contents: write
concurrency:
group: coverage
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Update coverage report
uses: ncruces/go-coverage-report@v0
with:
report: true
chart: true
amend: true
if: |
github.event_name == 'push'
continue-on-error: true

View file

@ -1,30 +0,0 @@
name: golangci-lint
on:
push:
branches: [ "master" ]
tags: [ "*" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
pull-requests: read
checks: write
jobs:
golangci:
strategy:
matrix:
go: [stable, oldstable]
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest

View file

@ -1,29 +0,0 @@
name: goreleaser
on:
push:
tags: [ "*" ]
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View file

@ -1,4 +1 @@
.idea
/fatcontext
dist/

View file

@ -1,18 +0,0 @@
version: "2"
issues:
fix: true
linters:
default: all
disable:
- depguard
- exhaustruct
formatters:
enable:
- goimports
- gofmt
- gofumpt
- golines
settings:
goimports:
local-prefixes:
- github.com/Crocmagnon/fatcontext

View file

@ -1,44 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
force_token: github
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: ./cmd/fatcontext
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
release:
draft: true

View file

@ -1,21 +0,0 @@
ci:
skip: [golangci-lint-full, go-test]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/golangci/golangci-lint
rev: v2.0.2
hooks:
- id: golangci-lint-full
- repo: local
hooks:
- id: go-test
name: go test ./...
language: golang
types_or: [go]
entry: make test

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Gabriel Augendre
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.

View file

@ -1,7 +0,0 @@
.PHONY: lint
lint:
pre-commit run --all-files
.PHONY: test
test:
go test -race -v ./...

View file

@ -1,21 +1,8 @@
# fatcontext
# foreshadow
[![Go Reference](https://pkg.go.dev/badge/github.com/Crocmagnon/fatcontext.svg)](https://pkg.go.dev/github.com/Crocmagnon/fatcontext)
[![Go Report Card](https://goreportcard.com/badge/github.com/Crocmagnon/fatcontext)](https://goreportcard.com/report/github.com/Crocmagnon/fatcontext)
[![Go Coverage](https://github.com/Crocmagnon/fatcontext/wiki/coverage.svg)](https://github.com/Crocmagnon/fatcontext/wiki/Coverage)
`fatcontext` is a Go linter which detects potential fat contexts in loops or function literals.
`foreshadow` is a Go linter which detects un-shadowed contexts in loops.
They can lead to performance issues, as documented here: https://gabnotes.org/fat-contexts/
## Installation / usage
`fatcontext` is available in `golangci-lint` since v1.58.0.
```bash
go install github.com/Crocmagnon/fatcontext/cmd/fatcontext@latest
fatcontext ./...
```
## Example
```go
@ -36,26 +23,8 @@ func notOk() {
ctx := context.Background()
for i := 0; i < 10; i++ {
ctx = context.WithValue(ctx, "key", i) // "nested context in loop"
ctx = context.WithValue(ctx, "key", i) // "context not shadowed in loop"
_ = ctx
}
}
```
## Development
Setup pre-commit locally:
```bash
pre-commit install
```
Run tests & linter:
```bash
make lint test
```
To release, just publish a git tag:
```bash
git tag -a v0.1.0 -m "v0.1.0"
git push --follow-tags
```

View file

@ -1,12 +0,0 @@
// Package main runs the analyzer. It's the CLI entrypoint.
package main
import (
"golang.org/x/tools/go/analysis/singlechecker"
"github.com/Crocmagnon/fatcontext/pkg/analyzer"
)
func main() {
singlechecker.Main(analyzer.NewAnalyzer())
}

10
cmd/foreshadow/main.go Normal file
View file

@ -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)
}

21
contrib/example.go Normal file
View file

@ -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) // "context not shadowed in loop"
_ = ctx
}
}

11
go.mod
View file

@ -1,10 +1,7 @@
module github.com/Crocmagnon/fatcontext
module github.com/Crocmagnon/foreshadow
go 1.23.0
go 1.21
require golang.org/x/tools v0.31.0
require golang.org/x/tools v0.19.0
require (
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect
)
require golang.org/x/mod v0.16.0 // indirect

14
go.sum
View file

@ -1,8 +1,6 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
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=

View file

@ -1,69 +1,32 @@
// Package analyzer contains everything related to the linter analysis.
package analyzer
import (
"bytes"
"errors"
"flag"
"fmt"
"go/ast"
"go/printer"
"go/token"
"slices"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
// FlagCheckStructPointers is a possible flag for the analyzer.
// Exported to make it usable in golangci-lint.
const FlagCheckStructPointers = "check-struct-pointers"
// NewAnalyzer returns a fatcontext analyzer.
func NewAnalyzer() *analysis.Analyzer {
rnnr := &runner{}
flags := flag.NewFlagSet("fatcontext", flag.ExitOnError)
flags.BoolVar(&rnnr.DetectInStructPointers, FlagCheckStructPointers, false,
"set to true to detect potential fat contexts in struct pointers")
return &analysis.Analyzer{
Name: "fatcontext",
Doc: "detects nested contexts in loops and function literals",
Run: rnnr.run,
Flags: *flags,
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
var Analyzer = &analysis.Analyzer{
Name: "foreshadow",
Doc: "enforce context shadowing inside loops",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
var (
errUnknown = errors.New("unknown node type")
errInvalidAnalysis = errors.New("invalid analysis")
)
var errUnknown = errors.New("unknown node type")
const (
categoryInLoop = "nested context in loop"
categoryInFuncLit = "nested context in function literal"
categoryInStructPointer = "potential nested context in struct pointer"
categoryUnsupported = "unsupported nested context type"
)
type runner struct {
DetectInStructPointers bool
}
func (r *runner) run(pass *analysis.Pass) (interface{}, error) {
inspctr, typeValid := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !typeValid {
return nil, errInvalidAnalysis
}
func run(pass *analysis.Pass) (interface{}, error) {
inspctr := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.ForStmt)(nil),
(*ast.RangeStmt)(nil),
(*ast.FuncLit)(nil),
(*ast.FuncDecl)(nil),
}
inspctr.Preorder(nodeFilter, func(node ast.Node) {
@ -72,245 +35,71 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) {
return
}
if body == 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
}
assignStmt := findNestedContext(pass, node, body.List)
if assignStmt == nil {
return
}
category := getCategory(pass, node, assignStmt)
if r.shouldIgnoreReport(category) {
return
}
fixes := r.getSuggestedFixes(pass, assignStmt, category)
pass.Report(analysis.Diagnostic{
Pos: assignStmt.Pos(),
Message: category,
SuggestedFixes: fixes,
})
})
return nil, nil //nolint:nilnil // we have no result to send to other analyzers
}
func (r *runner) shouldIgnoreReport(category string) bool {
return category == categoryInStructPointer && !r.DetectInStructPointers
}
func (r *runner) getSuggestedFixes(
pass *analysis.Pass,
assignStmt *ast.AssignStmt,
category string,
) []analysis.SuggestedFix {
switch category {
case categoryInStructPointer, categoryUnsupported:
return nil
}
suggestedStmt := ast.AssignStmt{
Lhs: assignStmt.Lhs,
TokPos: assignStmt.TokPos,
Tok: token.DEFINE,
Rhs: assignStmt.Rhs,
}
suggested, err := render(pass.Fset, &suggestedStmt)
var fixes []analysis.SuggestedFix
if err == nil {
fixes = append(fixes, analysis.SuggestedFix{
Message: "replace `=` with `:=`",
TextEdits: []analysis.TextEdit{
{
Pos: assignStmt.Pos(),
End: assignStmt.End(),
NewText: suggested,
},
},
})
}
return fixes
}
func getCategory(pass *analysis.Pass, node ast.Node, assignStmt *ast.AssignStmt) string {
switch node.(type) {
case *ast.ForStmt, *ast.RangeStmt:
return categoryInLoop
}
if isPointer(pass, assignStmt.Lhs[0]) {
return categoryInStructPointer
}
switch node.(type) {
case *ast.FuncLit, *ast.FuncDecl:
return categoryInFuncLit
default:
return categoryUnsupported
}
return nil, nil
}
func getBody(node ast.Node) (*ast.BlockStmt, error) {
switch typedNode := node.(type) {
case *ast.ForStmt:
return typedNode.Body, nil
case *ast.RangeStmt:
return typedNode.Body, nil
case *ast.FuncLit:
return typedNode.Body, nil
case *ast.FuncDecl:
return typedNode.Body, nil
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
}
func findNestedContext(pass *analysis.Pass, node ast.Node, stmts []ast.Stmt) *ast.AssignStmt {
for _, stmt := range stmts {
// Recurse if necessary
stmtList := getStmtList(stmt)
if found := findNestedContext(pass, node, stmtList); found != nil {
return found
}
// Actually check for nested context
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 {
continue
}
// Ignore [context.Background] & [context.TODO].
if isContextFunction(assignStmt.Rhs[0], "Background", "TODO") {
continue
}
if isPointer(pass, assignStmt.Lhs[0]) {
return assignStmt
}
// allow assignment to non-pointer children of values defined within the loop
if isWithinLoop(assignStmt.Lhs[0], node, pass) {
continue
}
return assignStmt
}
return nil
}
func getStmtList(stmt ast.Stmt) []ast.Stmt {
switch typedStmt := stmt.(type) {
case *ast.BlockStmt:
return typedStmt.List
case *ast.IfStmt:
return typedStmt.Body.List
case *ast.SwitchStmt:
return typedStmt.Body.List
case *ast.CaseClause:
return typedStmt.Body
case *ast.SelectStmt:
return typedStmt.Body.List
case *ast.CommClause:
return typedStmt.Body
}
return nil
}
// render returns the pretty-print of the given node.
func render(fset *token.FileSet, x interface{}) ([]byte, error) {
// 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 {
return nil, fmt.Errorf("printing node: %w", err)
panic(err)
}
return buf.Bytes(), nil
}
func isContextFunction(exp ast.Expr, fnName ...string) bool {
call, typeValid := exp.(*ast.CallExpr)
if !typeValid {
return false
}
selector, typeValid := call.Fun.(*ast.SelectorExpr)
if !typeValid {
return false
}
ident, typeValid := selector.X.(*ast.Ident)
if !typeValid {
return false
}
return ident.Name == "context" && slices.Contains(fnName, selector.Sel.Name)
}
func isWithinLoop(exp ast.Expr, node ast.Node, pass *analysis.Pass) bool {
lhs := getRootIdent(pass, exp)
if lhs == nil {
return false
}
obj := pass.TypesInfo.ObjectOf(lhs)
if obj == nil {
return false
}
scope := obj.Parent()
if scope == nil {
return false
}
return scope.Pos() >= node.Pos() && scope.End() <= node.End()
}
func getRootIdent(pass *analysis.Pass, node ast.Node) *ast.Ident {
for {
switch typedNode := node.(type) {
case *ast.Ident:
return typedNode
case *ast.IndexExpr:
node = typedNode.X
case *ast.SelectorExpr:
if sel, ok := pass.TypesInfo.Selections[typedNode]; ok && sel.Indirect() {
return nil // indirected (pointer) roots don't imply a (safe) copy
}
node = typedNode.X
default:
return nil
}
}
}
func isPointer(pass *analysis.Pass, exp ast.Node) bool {
switch n := exp.(type) { //nolint:gocritic // Future-proofing with switch instead of if.
case *ast.SelectorExpr:
sel, ok := pass.TypesInfo.Selections[n]
return ok && sel.Indirect()
}
return false
return buf.String()
}

View file

@ -1,67 +1,19 @@
package analyzer_test
import (
"testing"
"github.com/Crocmagnon/foreshadow/pkg/analyzer"
"golang.org/x/tools/go/analysis/analysistest"
"github.com/Crocmagnon/fatcontext/pkg/analyzer"
"os"
"path/filepath"
"testing"
)
func TestAnalyzer(t *testing.T) {
t.Parallel()
testCases := []struct {
desc string
dir string
options map[string]string
}{
{
desc: "no func decl",
dir: "common",
},
{
desc: "no func decl",
dir: "no_structpointer",
},
{
desc: "func decl",
dir: "common",
options: map[string]string{
analyzer.FlagCheckStructPointers: "true",
},
},
{
desc: "func decl",
dir: "structpointer",
options: map[string]string{
analyzer.FlagCheckStructPointers: "true",
},
},
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")
for _, test := range testCases {
t.Run(test.desc+"_"+test.dir, func(t *testing.T) {
t.Parallel()
anlzr := analyzer.NewAnalyzer()
for k, v := range test.options {
err := anlzr.Flags.Set(k, v)
if err != nil {
t.Fatal(err)
}
}
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), anlzr, test.dir)
})
}
}
func TestAnalyzer_cgo(t *testing.T) {
t.Parallel()
a := analyzer.NewAnalyzer()
analysistest.Run(t, analysistest.TestData(), a, "cgo")
analysistest.Run(t, testdata, analyzer.Analyzer, "./...")
}

View file

@ -1,251 +0,0 @@
package common
import (
"context"
"testing"
)
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 "nested context in loop"
ctx = context.WithValue(ctx, "other", "val")
}
for item := range []string{"one", "two", "three"} {
ctx = wrapContext(ctx) // want "nested context in loop"
ctx := context.WithValue(ctx, "key", item)
ctx = wrapContext(ctx)
}
for {
ctx = wrapContext(ctx) // want "nested context in loop"
break
}
// not fooled by shadowing in nested blocks
for {
err := doSomething()
if err != nil {
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
switch err {
case nil:
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
default:
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
{
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
select {
case <-ctx.Done():
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
default:
}
ctx = wrapContext(ctx) // want "nested context in loop"
break
}
// detects contexts wrapped in function literals (this is risky as function literals can be called multiple times)
_ = func() {
ctx = wrapContext(ctx) // want "nested context in function literal"
}
// this is fine because the context is created in the loop
for {
if ctx := context.Background(); doSomething() != nil {
ctx = wrapContext(ctx)
}
}
for {
ctx2 := context.Background()
ctx = wrapContext(ctx) // want "nested context in loop"
if doSomething() != nil {
ctx2 = wrapContext(ctx2)
}
}
}
func wrapContext(ctx context.Context) context.Context {
return context.WithoutCancel(ctx)
}
func doSomething() error {
return nil
}
// storing contexts in a struct isn't recommended, but local copies of a non-pointer struct should act like local copies of a context.
func inStructs(ctx context.Context) {
for i := 0; i < 10; i++ {
c := struct{ Ctx context.Context }{ctx}
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
for i := 0; i < 10; i++ {
c := []struct{ Ctx context.Context }{{ctx}}
c[0].Ctx = context.WithValue(c[0].Ctx, "key", i)
c[0].Ctx = context.WithValue(c[0].Ctx, "other", "val")
}
c := struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := c
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
pc := &struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := pc
c.Ctx = context.WithValue(c.Ctx, "key", i) // want "nested context in loop"
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
r := []struct{ Ctx context.Context }{{ctx}}
for i := 0; i < 10; i++ {
r[0].Ctx = context.WithValue(r[0].Ctx, "key", i) // want "nested context in loop"
r[0].Ctx = context.WithValue(r[0].Ctx, "other", "val")
}
rp := []*struct{ Ctx context.Context }{{ctx}}
for i := 0; i < 10; i++ {
rp[0].Ctx = context.WithValue(rp[0].Ctx, "key", i) // want "nested context in loop"
rp[0].Ctx = context.WithValue(rp[0].Ctx, "other", "val")
}
}
func inVariousNestedBlocks(ctx context.Context) {
for {
err := doSomething()
if err != nil {
ctx = wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
err := doSomething()
if err != nil {
if true {
ctx = wrapContext(ctx) // want "nested context in loop"
}
}
break
}
for {
err := doSomething()
switch err {
case nil:
ctx = wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
err := doSomething()
switch err {
default:
ctx = wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
ctx := wrapContext(ctx)
err := doSomething()
if err != nil {
ctx = wrapContext(ctx)
}
break
}
for {
{
ctx = wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
select {
case <-ctx.Done():
ctx = wrapContext(ctx) // want "nested context in loop"
default:
}
break
}
}
// this middleware could run on every request, bloating the request parameter level context and causing a memory leak
func badMiddleware(ctx context.Context) func() error {
return func() error {
ctx = wrapContext(ctx) // want "nested context in function literal"
return doSomethingWithCtx(ctx)
}
}
// this middleware is fine, as it doesn't modify the context of parent function
func okMiddleware(ctx context.Context) func() error {
return func() error {
ctx := wrapContext(ctx)
return doSomethingWithCtx(ctx)
}
}
// this middleware is fine, as it only modifies the context passed to it
func okMiddleware2(ctx context.Context) func(ctx context.Context) error {
return func(ctx context.Context) error {
ctx = wrapContext(ctx)
return doSomethingWithCtx(ctx)
}
}
func doSomethingWithCtx(ctx context.Context) error {
return nil
}
func testCasesInit(t *testing.T) {
cases := []struct {
ctx context.Context
}{
{},
{
ctx: context.WithValue(context.Background(), "key", "value"),
},
}
for _, tc := range cases {
t.Run("some test", func(t *testing.T) {
if tc.ctx == nil {
tc.ctx = context.Background()
}
})
}
}

View file

@ -1,251 +0,0 @@
package common
import (
"context"
"testing"
)
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 "nested context in loop"
ctx = context.WithValue(ctx, "other", "val")
}
for item := range []string{"one", "two", "three"} {
ctx := wrapContext(ctx) // want "nested context in loop"
ctx := context.WithValue(ctx, "key", item)
ctx = wrapContext(ctx)
}
for {
ctx := wrapContext(ctx) // want "nested context in loop"
break
}
// not fooled by shadowing in nested blocks
for {
err := doSomething()
if err != nil {
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
switch err {
case nil:
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
default:
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
{
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
}
select {
case <-ctx.Done():
ctx := wrapContext(ctx)
ctx = wrapContext(ctx)
default:
}
ctx := wrapContext(ctx) // want "nested context in loop"
break
}
// detects contexts wrapped in function literals (this is risky as function literals can be called multiple times)
_ = func() {
ctx := wrapContext(ctx) // want "nested context in function literal"
}
// this is fine because the context is created in the loop
for {
if ctx := context.Background(); doSomething() != nil {
ctx = wrapContext(ctx)
}
}
for {
ctx2 := context.Background()
ctx := wrapContext(ctx) // want "nested context in loop"
if doSomething() != nil {
ctx2 = wrapContext(ctx2)
}
}
}
func wrapContext(ctx context.Context) context.Context {
return context.WithoutCancel(ctx)
}
func doSomething() error {
return nil
}
// storing contexts in a struct isn't recommended, but local copies of a non-pointer struct should act like local copies of a context.
func inStructs(ctx context.Context) {
for i := 0; i < 10; i++ {
c := struct{ Ctx context.Context }{ctx}
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
for i := 0; i < 10; i++ {
c := []struct{ Ctx context.Context }{{ctx}}
c[0].Ctx = context.WithValue(c[0].Ctx, "key", i)
c[0].Ctx = context.WithValue(c[0].Ctx, "other", "val")
}
c := struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := c
c.Ctx = context.WithValue(c.Ctx, "key", i)
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
pc := &struct{ Ctx context.Context }{ctx}
for i := 0; i < 10; i++ {
c := pc
c.Ctx := context.WithValue(c.Ctx, "key", i) // want "nested context in loop"
c.Ctx = context.WithValue(c.Ctx, "other", "val")
}
r := []struct{ Ctx context.Context }{{ctx}}
for i := 0; i < 10; i++ {
r[0].Ctx := context.WithValue(r[0].Ctx, "key", i) // want "nested context in loop"
r[0].Ctx = context.WithValue(r[0].Ctx, "other", "val")
}
rp := []*struct{ Ctx context.Context }{{ctx}}
for i := 0; i < 10; i++ {
rp[0].Ctx := context.WithValue(rp[0].Ctx, "key", i) // want "nested context in loop"
rp[0].Ctx = context.WithValue(rp[0].Ctx, "other", "val")
}
}
func inVariousNestedBlocks(ctx context.Context) {
for {
err := doSomething()
if err != nil {
ctx := wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
err := doSomething()
if err != nil {
if true {
ctx := wrapContext(ctx) // want "nested context in loop"
}
}
break
}
for {
err := doSomething()
switch err {
case nil:
ctx := wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
err := doSomething()
switch err {
default:
ctx := wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
ctx := wrapContext(ctx)
err := doSomething()
if err != nil {
ctx = wrapContext(ctx)
}
break
}
for {
{
ctx := wrapContext(ctx) // want "nested context in loop"
}
break
}
for {
select {
case <-ctx.Done():
ctx := wrapContext(ctx) // want "nested context in loop"
default:
}
break
}
}
// this middleware could run on every request, bloating the request parameter level context and causing a memory leak
func badMiddleware(ctx context.Context) func() error {
return func() error {
ctx := wrapContext(ctx) // want "nested context in function literal"
return doSomethingWithCtx(ctx)
}
}
// this middleware is fine, as it doesn't modify the context of parent function
func okMiddleware(ctx context.Context) func() error {
return func() error {
ctx := wrapContext(ctx)
return doSomethingWithCtx(ctx)
}
}
// this middleware is fine, as it only modifies the context passed to it
func okMiddleware2(ctx context.Context) func(ctx context.Context) error {
return func(ctx context.Context) error {
ctx = wrapContext(ctx)
return doSomethingWithCtx(ctx)
}
}
func doSomethingWithCtx(ctx context.Context) error {
return nil
}
func testCasesInit(t *testing.T) {
cases := []struct {
ctx context.Context
}{
{},
{
ctx: context.WithValue(context.Background(), "key", "value"),
},
}
for _, tc := range cases {
t.Run("some test", func(t *testing.T) {
if tc.ctx == nil {
tc.ctx = context.Background()
}
})
}
}

View file

@ -1,23 +0,0 @@
package common
import (
"context"
)
type Container struct {
Ctx context.Context
}
func something() func(*Container) {
return func(r *Container) {
ctx := r.Ctx
ctx = context.WithValue(ctx, "key", "val")
r.Ctx = ctx
}
}
func blah(r *Container) {
ctx := r.Ctx
ctx = context.WithValue(ctx, "key", "val")
r.Ctx = ctx
}

View file

@ -1,23 +0,0 @@
package common
import (
"context"
)
type Container struct {
Ctx context.Context
}
func something() func(*Container) {
return func(r *Container) {
ctx := r.Ctx
ctx = context.WithValue(ctx, "key", "val")
r.Ctx = ctx // want "potential nested context in struct pointer"
}
}
func blah(r *Container) {
ctx := r.Ctx
ctx = context.WithValue(ctx, "key", "val")
r.Ctx = ctx // want "potential nested context in struct pointer"
}

View file

@ -1,27 +1,8 @@
package cgo
package src
/*
#include <stdio.h>
#include <stdlib.h>
import "context"
void myprint(char* s) {
printf("%d\n", s);
}
*/
import "C"
import (
"context"
"unsafe"
)
func _() {
cs := C.CString("Hello from stdio\n")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}
func _() {
func example() {
ctx := context.Background()
for i := 0; i < 10; i++ {
@ -30,18 +11,18 @@ func _() {
}
for i := 0; i < 10; i++ {
ctx = context.WithValue(ctx, "key", i) // want "nested context in loop"
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 "nested context in loop"
ctx = wrapContext(ctx) // want "context not shadowed in loop"
ctx := context.WithValue(ctx, "key", item)
ctx = wrapContext(ctx)
}
for {
ctx = wrapContext(ctx) // want "nested context in loop"
ctx = wrapContext(ctx) // want "context not shadowed in loop"
break
}
}