Repository: sonatard/noctx Branch: master Commit: 00a60094e864 Files: 21 Total size: 34.1 KB Directory structure: gitextract_qb9kju5i/ ├── .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/ │ ├── crypto_tls/ │ │ └── tls.go │ ├── exec_cmd/ │ │ └── exec.go │ ├── http_client/ │ │ └── http_client.go │ ├── http_request/ │ │ └── http_request.go │ ├── httptest_request/ │ │ └── httptest_request.go │ ├── network/ │ │ └── net.go │ └── sql/ │ └── sql.go └── types.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: go: [ stable, oldstable ] name: Go ${{ matrix.go }} test steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Install GolangCI-Lint run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.5 - run: make lint - run: make test_coverage - name: Upload code coverage to codecov if: matrix.go == 'stable' uses: codecov/codecov-action@v3 with: file: ./coverage.out ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ coverage.out /noctx ================================================ FILE: .golangci.yml ================================================ version: "2" formatters: enable: - gofumpt - goimports linters: default: all disable: - exhaustruct - gochecknoglobals - gocognit - lll - mnd - nestif - nilnil - paralleltest - varnamelen settings: depguard: rules: main: deny: - pkg: github.com/instana/testify desc: not allowed - pkg: github.com/pkg/errors desc: Should be replaced by standard lib errors package govet: enable-all: true perfsprint: err-error: true errorf: true sprintf1: true strconcat: false exclusions: presets: - comments - common-false-positives - std-error-handling ================================================ FILE: .goreleaser.yml ================================================ version: 2 project_name: noctx builds: - binary: noctx main: ./cmd/noctx/main.go env: - CGO_ENABLED=0 flags: - -trimpath goos: - windows - darwin - linux goarch: - amd64 - 386 - arm - arm64 goarm: - 7 - 6 - 5 ignore: - goos: darwin goarch: 386 - goos: windows goarch: arm archives: - id: noctx name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' formats: [ 'tar.gz' ] format_overrides: - goos: windows formats: [ 'zip' ] files: - LICENSE ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 sonatard 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. ================================================ FILE: Makefile ================================================ .PHONY: all fmt test lint build build: go build -ldflags "-s -w" -trimpath ./cmd/noctx/ fmt: golangci-lint fmt ./... lint: golangci-lint run ./... test: go test -race ./... test_coverage: go test -race -coverprofile=coverage.out -covermode=atomic ./... ================================================ FILE: README.md ================================================ # noctx ![](https://github.com/sonatard/noctx/workflows/CI/badge.svg) `noctx` finds function calls without context.Context. Passing `context.Context` enables library user to cancel request, getting trace information and so on. `noctx` helps you to identify code that could be rewritten to use the context.Context. ## Usage ### noctx with go vet go vet is a Go standard tool for analyzing source code. 1. Install noctx. ```sh $ go install github.com/sonatard/noctx/cmd/noctx@latest ``` 2. Execute noctx ```sh $ go vet -vettool=`which noctx` main.go ./main.go:6:11: net/http.Get must not be called ``` ### noctx with golangci-lint golangci-lint is a fast Go linters runner. 1. Install golangci-lint. [golangci-lint - Install](https://golangci-lint.run/usage/install/) 2. Setup .golangci.yml ```yaml: # Add noctx to enable linters. linters: enable: - noctx # Or enable-all is true. linters: default: all disable: - xxx # Add unused linter to disable linters. ``` 3. Execute noctx ```sh # Use .golangci.yml $ golangci-lint run # Only execute noctx golangci-lint run --enable-only noctx ``` ## net/http package ### Rules https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/noctx.go#L41-L50 ### Sample https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/http_client/http_client.go#L11 https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/http_request/http_request.go#L17 ### Reference - [net/http - NewRequest](https://pkg.go.dev/net/http#NewRequest) - [net/http - NewRequestWithContext](https://pkg.go.dev/net/http#NewRequestWithContext) - [net/http - Request.WithContext](https://pkg.go.dev/net/http#Request.WithContext) - [net/http/httptest - NewRequest](https://pkg.go.dev/net/http/httptest#NewRequest) ## net package ### Rules https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/noctx.go#L26-L39 ### Sample https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/network/net.go#L17 ### References - [net - ListenConfig](https://pkg.go.dev/net#ListenConfig) - [net - Dialer.DialContext](https://pkg.go.dev/net#Dialer.DialContext) - [net - Resolver](https://pkg.go.dev/net#Resolver) - [net - DefaultResolver](https://pkg.go.dev/net#DefaultResolver) ## database/sql package ### Rules https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/noctx.go#L52-L66 ### Sample https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/sql/sql.go#L18 ### Reference - [database/sql](https://pkg.go.dev/database/sql) ## crypt/tls package ### Rules https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/noctx.go#L71-L74 ### Sample https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/crypto_tls/tls.go#L17 ### Reference - [crypto/tls - Dialer.DialContext](https://pkg.go.dev/crypto/tls#Dialer.DialContext) - [crypto/tls - Conn.HandshakeContext](https://pkg.go.dev/crypto/tls#Conn.HandshakeContext) ## exec package ### Rules https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/noctx.go#L68-L69 ### Sample https://github.com/sonatard/noctx/blob/b768dab1764733f7f69c5075b7497eff4c58f260/testdata/src/exec_cmd/exec.go#L11 ### Reference - [exec - exec.CommandContext](https://pkg.go.dev/exec#CommandContext) ================================================ FILE: cmd/noctx/main.go ================================================ package main import ( "github.com/sonatard/noctx" "golang.org/x/tools/go/analysis/unitchecker" ) func main() { unitchecker.Main(noctx.Analyzer) } ================================================ FILE: go.mod ================================================ module github.com/sonatard/noctx go 1.23.0 require ( github.com/gostaticanalysis/analysisutil v0.7.1 golang.org/x/tools v0.32.0 ) require ( github.com/gostaticanalysis/comment v1.5.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sync v0.13.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4 h1:d2/eIbH9XjD1fFwD5SHv8x168fjbQ9PB8hvs8DSEC08= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= ================================================ FILE: noctx.go ================================================ package noctx import ( "fmt" "maps" "slices" "github.com/gostaticanalysis/analysisutil" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/buildssa" ) var Analyzer = &analysis.Analyzer{ Name: "noctx", Doc: "noctx finds function calls without context.Context", Run: Run, RunDespiteErrors: false, Requires: []*analysis.Analyzer{ buildssa.Analyzer, }, ResultType: nil, FactTypes: nil, } var ngFuncMessages = map[string]string{ // net "net.Listen": "must not be called. use (*net.ListenConfig).Listen", "net.ListenPacket": "must not be called. use (*net.ListenConfig).ListenPacket", "net.Dial": "must not be called. use (*net.Dialer).DialContext", "net.DialTimeout": "must not be called. use (*net.Dialer).DialContext with (*net.Dialer).Timeout", "net.LookupCNAME": "must not be called. use (*net.Resolver).LookupCNAME with a context", "net.LookupHost": "must not be called. use (*net.Resolver).LookupHost with a context", "net.LookupIP": "must not be called. use (*net.Resolver).LookupIPAddr with a context", "net.LookupPort": "must not be called. use (*net.Resolver).LookupPort with a context", "net.LookupSRV": "must not be called. use (*net.Resolver).LookupSRV with a context", "net.LookupMX": "must not be called. use (*net.Resolver).LookupMX with a context", "net.LookupNS": "must not be called. use (*net.Resolver).LookupNS with a context", "net.LookupTXT": "must not be called. use (*net.Resolver).LookupTXT with a context", "net.LookupAddr": "must not be called. use (*net.Resolver).LookupAddr with a context", // net/http "net/http.Get": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", "net/http.Head": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", "net/http.Post": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", "net/http.PostForm": "must not be called. use net/http.NewRequestWithContext and (*net/http.Client).Do(*http.Request)", "(*net/http.Client).Get": "must not be called. use (*net/http.Client).Do(*http.Request)", "(*net/http.Client).Head": "must not be called. use (*net/http.Client).Do(*http.Request)", "(*net/http.Client).Post": "must not be called. use (*net/http.Client).Do(*http.Request)", "(*net/http.Client).PostForm": "must not be called. use (*net/http.Client).Do(*http.Request)", "net/http.NewRequest": "must not be called. use net/http.NewRequestWithContext", "net/http/httptest.NewRequest": "must not be called. use net/http/httptest.NewRequestWithContext", // database/sql "(*database/sql.DB).Begin": "must not be called. use (*database/sql.DB).BeginTx", "(*database/sql.DB).Exec": "must not be called. use (*database/sql.DB).ExecContext", "(*database/sql.DB).Ping": "must not be called. use (*database/sql.DB).PingContext", "(*database/sql.DB).Prepare": "must not be called. use (*database/sql.DB).PrepareContext", "(*database/sql.DB).Query": "must not be called. use (*database/sql.DB).QueryContext", "(*database/sql.DB).QueryRow": "must not be called. use (*database/sql.DB).QueryRowContext", "(*database/sql.Tx).Exec": "must not be called. use (*database/sql.Tx).ExecContext", "(*database/sql.Tx).Prepare": "must not be called. use (*database/sql.Tx).PrepareContext", "(*database/sql.Tx).Query": "must not be called. use (*database/sql.Tx).QueryContext", "(*database/sql.Tx).QueryRow": "must not be called. use (*database/sql.Tx).QueryRowContext", "(*database/sql.Tx).Stmt": "must not be called. use (*database/sql.Tx).StmtContext", "(*database/sql.Stmt).Exec": "must not be called. use (*database/sql.Conn).ExecContext", "(*database/sql.Stmt).Query": "must not be called. use (*database/sql.Conn).QueryContext", "(*database/sql.Stmt).QueryRow": "must not be called. use (*database/sql.Conn).QueryRowContext", // exec "os/exec.Command": "must not be called. use os/exec.CommandContext", // crypto/tls dialer "crypto/tls.Dial": "must not be called. use (*crypto/tls.Dialer).DialContext", "crypto/tls.DialWithDialer": "must not be called. use (*crypto/tls.Dialer).DialContext with NetDialer", "(*crypto/tls.Conn).Handshake": "must not be called. use (*crypto/tls.Conn).HandshakeContext", // log/slog is out of scope of this analyzer, as slog doesn't use the [context.Context] // for context cancellation, but for key-value logging via the data stored in it. // // Related discussion: https://github.com/sonatard/noctx/issues/47 } func Run(pass *analysis.Pass) (interface{}, error) { ngFuncs := typeFuncs(pass, slices.Collect(maps.Keys(ngFuncMessages))) if len(ngFuncs) == 0 { return nil, nil } ssa, ok := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) if !ok { panic(fmt.Sprintf("%T is not *buildssa.SSA", pass.ResultOf[buildssa.Analyzer])) } for _, sf := range ssa.SrcFuncs { for _, b := range sf.Blocks { for _, instr := range b.Instrs { for _, ngFunc := range ngFuncs { if analysisutil.Called(instr, nil, ngFunc) { pass.Reportf(instr.Pos(), "%s %s", ngFunc.FullName(), ngFuncMessages[ngFunc.FullName()]) break } } } } } return nil, nil } ================================================ FILE: noctx_test.go ================================================ package noctx_test import ( "testing" "github.com/sonatard/noctx" "golang.org/x/tools/go/analysis/analysistest" ) func TestAnalyzer(t *testing.T) { testCases := []struct { desc string }{ {desc: "crypto_tls"}, {desc: "exec_cmd"}, {desc: "http_client"}, {desc: "http_request"}, {desc: "httptest_request"}, {desc: "network"}, {desc: "sql"}, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { analysistest.Run(t, analysistest.TestData(), noctx.Analyzer, test.desc) }) } } ================================================ FILE: testdata/src/crypto_tls/tls.go ================================================ package crypto import ( "context" "crypto/tls" "net" ) func _() { ctx := context.Background() netDialer := &net.Dialer{} tlsConfig := &tls.Config{} // dialers tls.Dial("tcp", "localhost:8080", tlsConfig) // want `crypto/tls\.Dial must not be called. use \(\*crypto/tls\.Dialer\)\.DialContext` tls.DialWithDialer(netDialer, "tcp", "localhost:8080", tlsConfig) // want `crypto/tls\.DialWithDialer must not be called. use \(\*crypto/tls\.Dialer\)\.DialContext with NetDialer` tlsDialer := &tls.Dialer{ Config: tlsConfig, NetDialer: netDialer, } tlsDialer.DialContext(ctx, "tcp", "localhost:8080") // connection tlsConn := &tls.Conn{} tlsConn.Handshake() // want `\(\*crypto/tls\.Conn\)\.Handshake must not be called. use \(\*crypto/tls\.Conn\).HandshakeContext` tlsConn.HandshakeContext(ctx) } ================================================ FILE: testdata/src/exec_cmd/exec.go ================================================ package exec import ( "context" "os/exec" ) func _() { ctx := context.Background() exec.Command("ls", "-l") // want `os/exec.Command must not be called. use os/exec.CommandContext` exec.CommandContext(ctx, "ls", "-l") } ================================================ FILE: testdata/src/http_client/http_client.go ================================================ package http_client import ( "net/http" ) func _() { const url = "http://example.com" cli := &http.Client{} http.Get(url) // want `net/http\.Get must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` _ = http.Get // OK f := http.Get // OK f(url) // want `net/http\.Get must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` http.Head(url) // want `net/http\.Head must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` http.Post(url, "", nil) // want `net/http\.Post must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` http.PostForm(url, nil) // want `net/http\.PostForm must not be called. use net/http\.NewRequestWithContext and \(\*net/http.Client\)\.Do\(\*http.Request\)` cli.Get(url) // want `\(\*net/http\.Client\)\.Get must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` _ = cli.Get // OK m := cli.Get // OK m(url) // want `\(\*net/http\.Client\)\.Get must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` cli.Head(url) // want `\(\*net/http\.Client\)\.Head must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` cli.Post(url, "", nil) // want `\(\*net/http\.Client\)\.Post must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` cli.PostForm(url, nil) // want `\(\*net/http\.Client\)\.PostForm must not be called. use \(\*net/http.Client\)\.Do\(\*http.Request\)` } ================================================ FILE: testdata/src/http_request/http_request.go ================================================ package http_request import ( "context" "net/http" ) var newRequestPkg = http.NewRequest func _() { const url = "https://example.com" cli := &http.Client{} ctx := context.Background() req, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` cli.Do(req) req2, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) // OK cli.Do(req2) req3, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req3 = req3.WithContext(ctx) cli.Do(req3) f2 := func(req *http.Request, ctx context.Context) *http.Request { return req } req4, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req4 = f2(req4, ctx) req41, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req41 = req41.WithContext(ctx) req41 = f2(req41, ctx) newRequest := http.NewRequest req5, _ := newRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` cli.Do(req5) req51, _ := newRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req51 = req51.WithContext(ctx) cli.Do(req51) req52, _ := newRequestPkg(http.MethodPost, url, nil) // TODO: false negative `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` cli.Do(req52) type MyRequest = http.Request f3 := func(req *MyRequest, ctx context.Context) *MyRequest { return req } req6, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req6 = f3(req6, ctx) req61, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req61 = req61.WithContext(ctx) req61 = f3(req61, ctx) type MyRequest2 http.Request f4 := func(req *MyRequest2, ctx context.Context) *MyRequest2 { return req } req7, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req71 := MyRequest2(*req7) f4(&req71, ctx) req72, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req72 = req72.WithContext(ctx) req73 := MyRequest2(*req7) f4(&req73, ctx) req8, _ := func() (*http.Request, error) { return http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` }() cli.Do(req8) req82, _ := func() (*http.Request, error) { req82, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req82 = req82.WithContext(ctx) return req82, nil }() cli.Do(req82) f5 := func(req, req2 *http.Request, ctx context.Context) (*http.Request, *http.Request) { return req, req2 } req9, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req9, _ = f5(req9, req9, ctx) req91, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req91 = req91.WithContext(ctx) req9, _ = f5(req91, req91, ctx) req10, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req11, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req10, req11 = f5(req10, req11, ctx) req101, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req111, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req111 = req111.WithContext(ctx) req101, req111 = f5(req101, req111, ctx) func() (*http.Request, *http.Request) { req12, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req13, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` return req12, req13 }() func() (*http.Request, *http.Request) { req14, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req15, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req15 = req15.WithContext(ctx) return req14, req15 }() req121, _ := http.NewRequest(http.MethodPost, url, nil) // want `net/http\.NewRequest must not be called. use net/http\.NewRequestWithContext` req121.AddCookie(&http.Cookie{Name: "k", Value: "v"}) req121 = req121.WithContext(context.WithValue(req121.Context(), struct{}{}, 0)) cli.Do(req121) } ================================================ FILE: testdata/src/httptest_request/httptest_request.go ================================================ package httptest_request import ( "context" "net/http" "net/http/httptest" ) func _() { const url = "https://example.com" cli := &http.Client{} ctx := context.Background() req := httptest.NewRequest(http.MethodPost, url, nil) // want `net/http/httptest\.NewRequest must not be called. use net/http/httptest\.NewRequestWithContext` cli.Do(req) req2 := httptest.NewRequestWithContext(ctx, http.MethodPost, url, nil) // OK cli.Do(req2) newRequest := httptest.NewRequest req3 := newRequest(http.MethodPost, url, nil) // want `net/http/httptest\.NewRequest must not be called. use net/http/httptest\.NewRequestWithContext` cli.Do(req3) } ================================================ FILE: testdata/src/network/net.go ================================================ package network import ( "context" "net" "time" ) var timeout = 10 * time.Second func _() { ctx := context.Background() // listenConfig listenConfig := &net.ListenConfig{} net.Listen("tcp", "localhost:8080") // want `net\.Listen must not be called. use \(\*net\.ListenConfig\)\.Listen` listenConfig.Listen(ctx, "tcp", "localhost:8080") net.ListenPacket("udp", "localhost:8080") // want `net\.ListenPacket must not be called. use \(\*net\.ListenConfig\)\.ListenPacket` listenConfig.ListenPacket(ctx, "udp", "localhost:8080") // dialer net.Dial("tcp", "localhost:8080") // want `net\.Dial must not be called. use \(\*net\.Dialer\)\.DialContext` net.DialTimeout("tcp", "localhost:8080", timeout) // want `net\.DialTimeout must not be called. use \(\*net\.Dialer\)\.DialContext with \(\*net\.Dialer\)\.Timeout` dialer := &net.Dialer{} dialer.DialContext(ctx, "tcp", "localhost:8080") dialerTimeout := &net.Dialer{Timeout: timeout} dialerTimeout.DialContext(ctx, "tcp", "localhost:8080") // resolver net.LookupCNAME("example.com") // want `net\.LookupCNAME must not be called. use \(\*net\.Resolver\)\.LookupCNAME with a context` net.LookupHost("example.com") // want `net\.LookupHost must not be called. use \(\*net\.Resolver\)\.LookupHost with a context` net.LookupIP("example.com") // want `net\.LookupIP must not be called. use \(\*net\.Resolver\)\.LookupIPAddr with a context` net.LookupPort("tcp", "http") // want `net\.LookupPort must not be called. use \(\*net\.Resolver\)\.LookupPort with a context` net.LookupSRV("http", "tcp", "example.com") // want `net\.LookupSRV must not be called. use \(\*net\.Resolver\)\.LookupSRV with a context` net.LookupMX("example.com") // want `net\.LookupMX must not be called. use \(\*net\.Resolver\)\.LookupMX with a context` net.LookupNS("example.com") // want `net\.LookupNS must not be called. use \(\*net\.Resolver\)\.LookupNS with a context` net.LookupTXT("example.com") // want `net\.LookupTXT must not be called. use \(\*net\.Resolver\)\.LookupTXT with a context` net.LookupAddr("example.com") // want `net\.LookupAddr must not be called. use \(\*net\.Resolver\)\.LookupAddr with a context` resolver := net.DefaultResolver resolver.LookupCNAME(ctx, "example.com") resolver.LookupHost(ctx, "example.com") resolver.LookupIPAddr(ctx, "example.com") resolver.LookupPort(ctx, "tcp", "http") resolver.LookupSRV(ctx, "http", "tcp", "example.com") resolver.LookupMX(ctx, "example.com") resolver.LookupNS(ctx, "example.com") resolver.LookupTXT(ctx, "example.com") resolver.LookupAddr(ctx, "example.com") } ================================================ FILE: testdata/src/sql/sql.go ================================================ package http_request import ( "context" "database/sql" ) func _() { ctx := context.Background() db, _ := sql.Open("noctx", "noctx://") // database/sql.DB methods db.Begin() // want `\(\*database/sql\.DB\)\.Begin must not be called. use \(\*database/sql\.DB\)\.BeginTx` db.BeginTx(ctx, nil) db.Exec("select * from testdata") // want `\(\*database/sql\.DB\)\.Exec must not be called. use \(\*database/sql\.DB\)\.ExecContext` db.ExecContext(ctx, "select * from testdata") db.Ping() // want `\(\*database/sql\.DB\)\.Ping must not be called. use \(\*database/sql\.DB\)\.PingContext` db.PingContext(ctx) db.Prepare("select * from testdata") // want `\(\*database/sql\.DB\)\.Prepare must not be called. use \(\*database/sql\.DB\)\.PrepareContext` db.PrepareContext(ctx, "select * from testdata") db.Query("select * from testdata") // want `\(\*database/sql\.DB\)\.Query must not be called. use \(\*database/sql\.DB\)\.QueryContext` db.QueryContext(ctx, "select * from testdata") db.QueryRow("select * from testdata") // want `\(\*database/sql\.DB\)\.QueryRow must not be called. use \(\*database/sql\.DB\)\.QueryRowContext` db.QueryRowContext(ctx, "select * from testdata") // database/sql.Stmt methods stmt, _ := db.PrepareContext(context.Background(), "select * from testdata where id = ?") stmt.Query("1") // want `\(\*database/sql\.Stmt\)\.Query must not be called. use \(\*database/sql\.Conn\)\.QueryContext` stmt.QueryContext(ctx, "1") stmt.QueryRow("1") // want `\(\*database/sql\.Stmt\)\.QueryRow must not be called. use \(\*database/sql\.Conn\)\.QueryRowContext` stmt.QueryRowContext(ctx, "1") stmt.Exec("1") // want `\(\*database/sql\.Stmt\)\.Exec must not be called. use \(\*database/sql\.Conn\)\.ExecContext` stmt.ExecContext(ctx, "1") // database/sql.Tx methods tx, _ := db.BeginTx(ctx, nil) tx.Exec("select * from testdata") // want `\(\*database/sql\.Tx\)\.Exec must not be called. use \(\*database/sql\.Tx\)\.ExecContext` tx.ExecContext(ctx, "select * from testdata") tx.Prepare("select * from testdata") // want `\(\*database/sql\.Tx\)\.Prepare must not be called. use \(\*database/sql\.Tx\)\.PrepareContext` tx.PrepareContext(ctx, "select * from testdata") tx.Query("select * from testdata") // want `\(\*database/sql\.Tx\)\.Query must not be called. use \(\*database/sql\.Tx\)\.QueryContext` tx.QueryContext(ctx, "select * from testdata") tx.QueryRow("select * from testdata") // want `\(\*database/sql\.Tx\)\.QueryRow must not be called. use \(\*database/sql\.Tx\)\.QueryRowContext` tx.QueryRowContext(ctx, "select * from testdata") tx.Stmt(stmt) // want `\(\*database/sql\.Tx\)\.Stmt must not be called. use \(\*database/sql\.Tx\)\.StmtContext` tx.StmtContext(ctx, stmt) _ = tx.Commit() // database/sql.Conn are safe, they only have context-aware methods // these lines are just to show that they are not flagged conn, _ := db.Conn(ctx) conn.ExecContext(ctx, "select * from testdata") conn.QueryContext(ctx, "select * from testdata") conn.QueryRowContext(ctx, "select * from testdata") } ================================================ FILE: types.go ================================================ package noctx import ( "errors" "go/types" "strings" "github.com/gostaticanalysis/analysisutil" "golang.org/x/tools/go/analysis" ) var errNotFound = errors.New("function not found") func typeFuncs(pass *analysis.Pass, funcs []string) []*types.Func { fs := make([]*types.Func, 0, len(funcs)) for _, fn := range funcs { f, err := typeFunc(pass, fn) if err != nil { continue } fs = append(fs, f) } return fs } func typeFunc(pass *analysis.Pass, funcName string) (*types.Func, error) { nameParts := strings.Split(strings.TrimSpace(funcName), ".") switch len(nameParts) { case 2: // package function: pkgname.Func f, ok := analysisutil.ObjectOf(pass, nameParts[0], nameParts[1]).(*types.Func) if !ok || f == nil { return nil, errNotFound } return f, nil case 3: // method: (*pkgname.Type).Method pkgName := strings.TrimLeft(nameParts[0], "(") typeName := strings.TrimRight(nameParts[1], ")") if pkgName != "" && pkgName[0] == '*' { pkgName = pkgName[1:] typeName = "*" + typeName } typ := analysisutil.TypeOf(pass, pkgName, typeName) if typ == nil { return nil, errNotFound } m := analysisutil.MethodOf(typ, nameParts[2]) if m == nil { return nil, errNotFound } return m, nil } return nil, errNotFound }