├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── noctx │ └── main.go ├── go.mod ├── go.sum ├── noctx.go ├── noctx_test.go ├── testdata └── src │ ├── http_client │ └── http_client.go │ ├── http_request │ └── http_request.go │ └── sql │ └── sql.go └── types.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go: [ stable, oldstable ] 15 | name: Go ${{ matrix.go }} test 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - name: Install GolangCI-Lint 22 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.5 23 | - run: make lint 24 | - run: make test_coverage 25 | - name: Upload code coverage to codecov 26 | if: matrix.go == 'stable' 27 | uses: codecov/codecov-action@v3 28 | with: 29 | file: ./coverage.out 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Unshallow 15 | run: git fetch --prune --unshallow 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: stable 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v6 22 | with: 23 | version: latest 24 | args: release --clean 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | /noctx 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | formatters: 4 | enable: 5 | - gofumpt 6 | - goimports 7 | 8 | 9 | linters: 10 | default: all 11 | disable: 12 | - exhaustruct 13 | - gochecknoglobals 14 | - gocognit 15 | - lll 16 | - mnd 17 | - nestif 18 | - nilnil 19 | - paralleltest 20 | - varnamelen 21 | 22 | settings: 23 | depguard: 24 | rules: 25 | main: 26 | deny: 27 | - pkg: github.com/instana/testify 28 | desc: not allowed 29 | - pkg: github.com/pkg/errors 30 | desc: Should be replaced by standard lib errors package 31 | govet: 32 | enable-all: true 33 | perfsprint: 34 | err-error: true 35 | errorf: true 36 | sprintf1: true 37 | strconcat: false 38 | 39 | exclusions: 40 | presets: 41 | - comments 42 | - common-false-positives 43 | - std-error-handling 44 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: noctx 3 | 4 | builds: 5 | - binary: noctx 6 | 7 | main: ./cmd/noctx/main.go 8 | env: 9 | - CGO_ENABLED=0 10 | flags: 11 | - -trimpath 12 | goos: 13 | - windows 14 | - darwin 15 | - linux 16 | goarch: 17 | - amd64 18 | - 386 19 | - arm 20 | - arm64 21 | goarm: 22 | - 7 23 | - 6 24 | - 5 25 | ignore: 26 | - goos: darwin 27 | goarch: 386 28 | 29 | archives: 30 | - id: noctx 31 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' 32 | formats: [ 'tar.gz' ] 33 | format_overrides: 34 | - goos: windows 35 | formats: [ 'zip' ] 36 | files: 37 | - LICENSE 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 sonatard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all fmt test lint build 2 | 3 | build: 4 | go build -ldflags "-s -w" -trimpath ./cmd/noctx/ 5 | 6 | fmt: 7 | golangci-lint fmt ./... 8 | 9 | lint: 10 | golangci-lint run ./... 11 | 12 | test: 13 | go test -race ./... 14 | 15 | test_coverage: 16 | go test -race -coverprofile=coverage.out -covermode=atomic ./... 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noctx 2 | 3 | ![](https://github.com/sonatard/noctx/workflows/CI/badge.svg) 4 | 5 | `noctx` finds function calls without context.Context. 6 | 7 | If you are using net/http package and sql/database package, you should use noctx. 8 | Passing `context.Context` enables library user to cancel request, getting trace information and so on. 9 | 10 | ## Usage 11 | 12 | ### noctx with go vet 13 | 14 | go vet is a Go standard tool for analyzing source code. 15 | 16 | 1. Install noctx. 17 | ```sh 18 | $ go install github.com/sonatard/noctx/cmd/noctx@latest 19 | ``` 20 | 21 | 2. Execute noctx 22 | ```sh 23 | $ go vet -vettool=`which noctx` main.go 24 | ./main.go:6:11: net/http.Get must not be called 25 | ``` 26 | 27 | ### noctx with golangci-lint 28 | 29 | golangci-lint is a fast Go linters runner. 30 | 31 | 1. Install golangci-lint. 32 | [golangci-lint - Install](https://golangci-lint.run/usage/install/) 33 | 34 | 2. Setup .golangci.yml 35 | ```yaml: 36 | # Add noctx to enable linters. 37 | linters: 38 | enable: 39 | - noctx 40 | 41 | # Or enable-all is true. 42 | linters: 43 | enable-all: true 44 | disable: 45 | - xxx # Add unused linter to disable linters. 46 | ``` 47 | 48 | 3. Execute noctx 49 | ```sh 50 | # Use .golangci.yml 51 | $ golangci-lint run 52 | 53 | # Only execute noctx 54 | golangci-lint run --enable-only noctx 55 | ``` 56 | 57 | ## net/http package 58 | ### Rules 59 | https://github.com/sonatard/noctx/blob/e9e23da29379b87a39ce50fd1ef7b273fee2461a/noctx.go#L28-L36 60 | 61 | ### Sample 62 | https://github.com/sonatard/noctx/blob/9a514098df3f8a88e0fd6949320c4e0aa51b520c/testdata/src/http_client/http_client.go#L11 63 | https://github.com/sonatard/noctx/blob/9a514098df3f8a88e0fd6949320c4e0aa51b520c/testdata/src/http_request/http_request.go#L17 64 | 65 | ### Reference 66 | - [net/http - NewRequest](https://pkg.go.dev/net/http#NewRequest) 67 | - [net/http - NewRequestWithContext](https://pkg.go.dev/net/http#NewRequestWithContext) 68 | - [net/http - Request.WithContext](https://pkg.go.dev/net/http#Request.WithContext) 69 | 70 | ## database/sql package 71 | ### Rules 72 | https://github.com/sonatard/noctx/blob/a00128b6a4087639ed0d13a123d0f9960309824f/noctx.go#L40-L48 73 | 74 | ### Sample 75 | https://github.com/sonatard/noctx/blob/6e0f6bb8de1bd8a3c6e73439614927fd59aa0a8a/testdata/src/sql/sql.go#L13 76 | 77 | ### Reference 78 | - [database/sql](https://pkg.go.dev/database/sql) 79 | -------------------------------------------------------------------------------- /cmd/noctx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sonatard/noctx" 5 | "golang.org/x/tools/go/analysis/unitchecker" 6 | ) 7 | 8 | func main() { unitchecker.Main(noctx.Analyzer) } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sonatard/noctx 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gostaticanalysis/analysisutil v0.7.1 7 | golang.org/x/tools v0.32.0 8 | ) 9 | 10 | require ( 11 | github.com/gostaticanalysis/comment v1.5.0 // indirect 12 | golang.org/x/mod v0.24.0 // indirect 13 | golang.org/x/sync v0.13.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= 6 | github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= 7 | github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= 8 | github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= 9 | github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= 10 | github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4 h1:d2/eIbH9XjD1fFwD5SHv8x168fjbQ9PB8hvs8DSEC08= 11 | github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= 12 | github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= 13 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 14 | github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= 15 | github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= 16 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 17 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 18 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 19 | github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 20 | github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= 21 | github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= 22 | github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= 23 | github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= 24 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 27 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 28 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 29 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 30 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 31 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 37 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 44 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 45 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 46 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 47 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 48 | golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= 49 | golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= 50 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 51 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | -------------------------------------------------------------------------------- /noctx.go: -------------------------------------------------------------------------------- 1 | package noctx 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "slices" 7 | 8 | "github.com/gostaticanalysis/analysisutil" 9 | "golang.org/x/tools/go/analysis" 10 | "golang.org/x/tools/go/analysis/passes/buildssa" 11 | ) 12 | 13 | var Analyzer = &analysis.Analyzer{ 14 | Name: "noctx", 15 | Doc: "noctx finds sending http request without context.Context", 16 | Run: Run, 17 | RunDespiteErrors: false, 18 | Requires: []*analysis.Analyzer{ 19 | buildssa.Analyzer, 20 | }, 21 | ResultType: nil, 22 | FactTypes: nil, 23 | } 24 | 25 | func Run(pass *analysis.Pass) (interface{}, error) { 26 | ngFuncMessages := map[string]string{ 27 | // net/http 28 | "net/http.Get": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", 29 | "net/http.Head": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", 30 | "net/http.Post": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", 31 | "net/http.PostForm": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", 32 | "(*net/http.Client).Get": "must not be called. use (*net/http.Client).Do(*http.Request)", 33 | "(*net/http.Client).Head": "must not be called. use (*net/http.Client).Do(*http.Request)", 34 | "(*net/http.Client).Post": "must not be called. use (*net/http.Client).Do(*http.Request)", 35 | "(*net/http.Client).PostForm": "must not be called. use (*net/http.Client).Do(*http.Request)", 36 | "net/http.NewRequest": "must not be called. use net/http.NewRequestWithContext", 37 | 38 | // database/sql 39 | "(*database/sql.DB).Exec": "must not be called. use (*database/sql.DB).ExecContext", 40 | "(*database/sql.DB).Ping": "must not be called. use (*database/sql.DB).PingContext", 41 | "(*database/sql.DB).Prepare": "must not be called. use (*database/sql.DB).PrepareContext", 42 | "(*database/sql.DB).Query": "must not be called. use (*database/sql.DB).QueryContext", 43 | "(*database/sql.DB).QueryRow": "must not be called. use (*database/sql.DB).QueryRowContext", 44 | "(*database/sql.Tx).Exec": "must not be called. use (*database/sql.Tx).ExecContext", 45 | "(*database/sql.Tx).Prepare": "must not be called. use (*database/sql.Tx).PrepareContext", 46 | "(*database/sql.Tx).Query": "must not be called. use (*database/sql.Tx).QueryContext", 47 | "(*database/sql.Tx).QueryRow": "must not be called. use (*database/sql.Tx).QueryRowContext", 48 | } 49 | 50 | ngFuncs := typeFuncs(pass, slices.Collect(maps.Keys(ngFuncMessages))) 51 | if len(ngFuncs) == 0 { 52 | return nil, nil 53 | } 54 | 55 | ssa, ok := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 56 | if !ok { 57 | panic(fmt.Sprintf("%T is not *buildssa.SSA", pass.ResultOf[buildssa.Analyzer])) 58 | } 59 | 60 | for _, sf := range ssa.SrcFuncs { 61 | for _, b := range sf.Blocks { 62 | for _, instr := range b.Instrs { 63 | for _, ngFunc := range ngFuncs { 64 | if analysisutil.Called(instr, nil, ngFunc) { 65 | pass.Reportf(instr.Pos(), "%s %s", ngFunc.FullName(), ngFuncMessages[ngFunc.FullName()]) 66 | 67 | break 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | return nil, nil 75 | } 76 | -------------------------------------------------------------------------------- /noctx_test.go: -------------------------------------------------------------------------------- 1 | package noctx_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sonatard/noctx" 7 | "golang.org/x/tools/go/analysis/analysistest" 8 | ) 9 | 10 | func TestAnalyzer(t *testing.T) { 11 | testCases := []struct { 12 | desc string 13 | }{ 14 | {desc: "http_client"}, 15 | {desc: "http_request"}, 16 | {desc: "sql"}, 17 | } 18 | 19 | for _, test := range testCases { 20 | t.Run(test.desc, func(t *testing.T) { 21 | analysistest.Run(t, analysistest.TestData(), noctx.Analyzer, test.desc) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/src/http_client/http_client.go: -------------------------------------------------------------------------------- 1 | package http_client 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func _() { 8 | const url = "http://example.com" 9 | cli := &http.Client{} 10 | 11 | http.Get(url) // want `net/http\.Get must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` 12 | _ = http.Get // OK 13 | f := http.Get // OK 14 | f(url) // want `net/http\.Get must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` 15 | 16 | http.Head(url) // want `net/http\.Head must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` 17 | http.Post(url, "", nil) // want `net/http\.Post must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` 18 | http.PostForm(url, nil) // want `net/http\.PostForm must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` 19 | 20 | cli.Get(url) // want `\(\*net/http\.Client\)\.Get must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` 21 | _ = cli.Get // OK 22 | m := cli.Get // OK 23 | m(url) // want `\(\*net/http\.Client\)\.Get must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` 24 | 25 | cli.Head(url) // want `\(\*net/http\.Client\)\.Head must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` 26 | cli.Post(url, "", nil) // want `\(\*net/http\.Client\)\.Post must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` 27 | cli.PostForm(url, nil) // want `\(\*net/http\.Client\)\.PostForm must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` 28 | } 29 | -------------------------------------------------------------------------------- /testdata/src/http_request/http_request.go: -------------------------------------------------------------------------------- 1 | package http_request 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | var newRequestPkg = http.NewRequest 9 | 10 | func _() { 11 | const url = "https://example.com" 12 | 13 | cli := &http.Client{} 14 | 15 | ctx := context.Background() 16 | 17 | req, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 18 | cli.Do(req) 19 | 20 | req2, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) // OK 21 | cli.Do(req2) 22 | 23 | req3, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 24 | req3 = req3.WithContext(ctx) 25 | cli.Do(req3) 26 | 27 | f2 := func(req *http.Request, ctx context.Context) *http.Request { 28 | return req 29 | } 30 | req4, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 31 | req4 = f2(req4, ctx) 32 | 33 | req41, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 34 | req41 = req41.WithContext(ctx) 35 | req41 = f2(req41, ctx) 36 | 37 | newRequest := http.NewRequest 38 | req5, _ := newRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 39 | cli.Do(req5) 40 | 41 | req51, _ := newRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 42 | req51 = req51.WithContext(ctx) 43 | cli.Do(req51) 44 | 45 | req52, _ := newRequestPkg(http.MethodPost, url, nil) // TODO: false negative `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 46 | cli.Do(req52) 47 | 48 | type MyRequest = http.Request 49 | f3 := func(req *MyRequest, ctx context.Context) *MyRequest { 50 | return req 51 | } 52 | req6, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 53 | req6 = f3(req6, ctx) 54 | 55 | req61, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 56 | req61 = req61.WithContext(ctx) 57 | req61 = f3(req61, ctx) 58 | 59 | type MyRequest2 http.Request 60 | f4 := func(req *MyRequest2, ctx context.Context) *MyRequest2 { 61 | return req 62 | } 63 | req7, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 64 | req71 := MyRequest2(*req7) 65 | f4(&req71, ctx) 66 | 67 | req72, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 68 | req72 = req72.WithContext(ctx) 69 | req73 := MyRequest2(*req7) 70 | f4(&req73, ctx) 71 | 72 | req8, _ := func() (*http.Request, error) { 73 | return http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 74 | }() 75 | cli.Do(req8) 76 | 77 | req82, _ := func() (*http.Request, error) { 78 | req82, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 79 | req82 = req82.WithContext(ctx) 80 | return req82, nil 81 | }() 82 | cli.Do(req82) 83 | 84 | f5 := func(req, req2 *http.Request, ctx context.Context) (*http.Request, *http.Request) { 85 | return req, req2 86 | } 87 | req9, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 88 | req9, _ = f5(req9, req9, ctx) 89 | 90 | req91, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 91 | req91 = req91.WithContext(ctx) 92 | req9, _ = f5(req91, req91, ctx) 93 | 94 | req10, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 95 | req11, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 96 | req10, req11 = f5(req10, req11, ctx) 97 | 98 | req101, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 99 | req111, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 100 | req111 = req111.WithContext(ctx) 101 | req101, req111 = f5(req101, req111, ctx) 102 | 103 | func() (*http.Request, *http.Request) { 104 | req12, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 105 | req13, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 106 | return req12, req13 107 | }() 108 | 109 | func() (*http.Request, *http.Request) { 110 | req14, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 111 | req15, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 112 | req15 = req15.WithContext(ctx) 113 | 114 | return req14, req15 115 | }() 116 | 117 | req121, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` 118 | req121.AddCookie(&http.Cookie{Name: "k", Value: "v"}) 119 | req121 = req121.WithContext(context.WithValue(req121.Context(), struct{}{}, 0)) 120 | cli.Do(req121) 121 | } 122 | -------------------------------------------------------------------------------- /testdata/src/sql/sql.go: -------------------------------------------------------------------------------- 1 | package http_request 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | func _() { 9 | ctx := context.Background() 10 | 11 | db, _ := sql.Open("noctx", "noctx://") 12 | 13 | db.Exec("select * from testdata") // want `\(\*database/sql\.DB\)\.Exec must not be called. use \(\*database/sql\.DB\)\.ExecContext` 14 | db.ExecContext(ctx, "select * from testdata") 15 | 16 | db.Ping() // want `\(\*database/sql\.DB\)\.Ping must not be called. use \(\*database/sql\.DB\)\.PingContext` 17 | db.PingContext(ctx) 18 | 19 | db.Prepare("select * from testdata") // want `\(\*database/sql\.DB\)\.Prepare must not be called. use \(\*database/sql\.DB\)\.PrepareContext` 20 | db.PrepareContext(ctx, "select * from testdata") 21 | 22 | db.Query("select * from testdata") // want `\(\*database/sql\.DB\)\.Query must not be called. use \(\*database/sql\.DB\)\.QueryContext` 23 | db.QueryContext(ctx, "select * from testdata") 24 | 25 | db.QueryRow("select * from testdata") // want `\(\*database/sql\.DB\)\.QueryRow must not be called. use \(\*database/sql\.DB\)\.QueryRowContext` 26 | db.QueryRowContext(ctx, "select * from testdata") 27 | 28 | // transactions 29 | 30 | tx, _ := db.Begin() 31 | tx.Exec("select * from testdata") // want `\(\*database/sql\.Tx\)\.Exec must not be called. use \(\*database/sql\.Tx\)\.ExecContext` 32 | tx.ExecContext(ctx, "select * from testdata") 33 | 34 | tx.Prepare("select * from testdata") // want `\(\*database/sql\.Tx\)\.Prepare must not be called. use \(\*database/sql\.Tx\)\.PrepareContext` 35 | tx.PrepareContext(ctx, "select * from testdata") 36 | 37 | tx.Query("select * from testdata") // want `\(\*database/sql\.Tx\)\.Query must not be called. use \(\*database/sql\.Tx\)\.QueryContext` 38 | tx.QueryContext(ctx, "select * from testdata") 39 | 40 | tx.QueryRow("select * from testdata") // want `\(\*database/sql\.Tx\)\.QueryRow must not be called. use \(\*database/sql\.Tx\)\.QueryRowContext` 41 | tx.QueryRowContext(ctx, "select * from testdata") 42 | 43 | _ = tx.Commit() 44 | } 45 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package noctx 2 | 3 | import ( 4 | "errors" 5 | "go/types" 6 | "strings" 7 | 8 | "github.com/gostaticanalysis/analysisutil" 9 | "golang.org/x/tools/go/analysis" 10 | ) 11 | 12 | var errNotFound = errors.New("function not found") 13 | 14 | func typeFuncs(pass *analysis.Pass, funcs []string) []*types.Func { 15 | fs := make([]*types.Func, 0, len(funcs)) 16 | 17 | for _, fn := range funcs { 18 | f, err := typeFunc(pass, fn) 19 | if err != nil { 20 | continue 21 | } 22 | 23 | fs = append(fs, f) 24 | } 25 | 26 | return fs 27 | } 28 | 29 | func typeFunc(pass *analysis.Pass, funcName string) (*types.Func, error) { 30 | nameParts := strings.Split(strings.TrimSpace(funcName), ".") 31 | 32 | switch len(nameParts) { 33 | case 2: 34 | // package function: pkgname.Func 35 | f, ok := analysisutil.ObjectOf(pass, nameParts[0], nameParts[1]).(*types.Func) 36 | if !ok || f == nil { 37 | return nil, errNotFound 38 | } 39 | 40 | return f, nil 41 | case 3: 42 | // method: (*pkgname.Type).Method 43 | pkgName := strings.TrimLeft(nameParts[0], "(") 44 | typeName := strings.TrimRight(nameParts[1], ")") 45 | 46 | if pkgName != "" && pkgName[0] == '*' { 47 | pkgName = pkgName[1:] 48 | typeName = "*" + typeName 49 | } 50 | 51 | typ := analysisutil.TypeOf(pass, pkgName, typeName) 52 | if typ == nil { 53 | return nil, errNotFound 54 | } 55 | 56 | m := analysisutil.MethodOf(typ, nameParts[2]) 57 | if m == nil { 58 | return nil, errNotFound 59 | } 60 | 61 | return m, nil 62 | } 63 | 64 | return nil, errNotFound 65 | } 66 | --------------------------------------------------------------------------------