├── .github ├── dependabot.yml └── workflows │ ├── go-cross.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── contextcheck │ └── main.go ├── contextcheck.go ├── contextcheck_test.go ├── go.mod ├── go.sum └── testdata └── src └── a └── a.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/go-cross.yml: -------------------------------------------------------------------------------- 1 | name: Go Matrix 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | cross: 7 | name: Go 8 | runs-on: ${{ matrix.os }} 9 | env: 10 | CGO_ENABLED: 0 11 | 12 | strategy: 13 | matrix: 14 | go-version: [ oldstable, stable ] 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | 17 | steps: 18 | # https://github.com/marketplace/actions/checkout 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | # https://github.com/marketplace/actions/setup-go-environment 23 | - name: Set up Go ${{ matrix.go-version }} 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go-version }} 27 | 28 | - name: Test 29 | run: go test -v -cover ./... 30 | 31 | - name: Build 32 | run: go build -v -ldflags "-s -w" -trimpath 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | main: 12 | name: Main Process 13 | runs-on: ubuntu-latest 14 | env: 15 | GO_VERSION: stable 16 | GOLANGCI_LINT_VERSION: v1.57.0 17 | CGO_ENABLED: 0 18 | 19 | steps: 20 | # https://github.com/marketplace/actions/checkout 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | # https://github.com/marketplace/actions/setup-go-environment 27 | - name: Set up Go ${{ env.GO_VERSION }} 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: ${{ env.GO_VERSION }} 31 | 32 | - name: Check and get dependencies 33 | run: | 34 | go mod download 35 | go mod tidy 36 | git diff --exit-code go.mod 37 | git diff --exit-code go.sum 38 | 39 | # https://golangci-lint.run/usage/install#other-ci 40 | - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} 41 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} 42 | 43 | - name: Make 44 | run: make 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | .DS_Store 19 | 20 | /contextcheck 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 sylvia.wang 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean test build 2 | 3 | default: test build 4 | 5 | clean: 6 | rm -rf dist/ cover.out 7 | 8 | test: clean 9 | go test -v -cover ./... 10 | 11 | build: 12 | go build -ldflags '-s -w' -o contextcheck ./cmd/contextcheck/main.go 13 | 14 | install: 15 | go install -ldflags '-s -w' ./cmd/contextcheck 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/sylvia7788/contextcheck.svg?style=svg)](https://circleci.com/gh/sylvia7788/contextcheck) 2 | 3 | 4 | # contextcheck 5 | 6 | `contextcheck` is a static analysis tool used to check whether a function uses a non-inherited context that could result in a broken call link. 7 | 8 | For example: 9 | 10 | ```go 11 | func call1(ctx context.Context) { 12 | ... 13 | 14 | ctx = getNewCtx(ctx) 15 | call2(ctx) // OK 16 | 17 | call2(context.Background()) // Non-inherited new context, use function like `context.WithXXX` instead 18 | 19 | call3() // Function `call3` should pass the context parameter 20 | call4() // Function `call4->call3` should pass the context parameter 21 | ... 22 | } 23 | 24 | func call2(ctx context.Context) { 25 | ... 26 | } 27 | 28 | func call3() { 29 | ctx := context.TODO() 30 | call2(ctx) 31 | } 32 | 33 | func call4() { 34 | call3() 35 | } 36 | 37 | 38 | // if you want none-inherit ctx, use this function 39 | func getNewCtx(ctx context.Context) (newCtx context.Context) { 40 | ... 41 | return 42 | } 43 | 44 | /* ---------- check net/http.HandleFunc ---------- */ 45 | 46 | func call5(ctx context.Context, w http.ResponseWriter, r *http.Request) { 47 | } 48 | 49 | func call6(w http.ResponseWriter, r *http.Request) { 50 | ctx := r.Context() 51 | call5(ctx, w, r) 52 | call5(context.Background(), w, r) // Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead 53 | } 54 | 55 | func call7(in bool, w http.ResponseWriter, r *http.Request) { 56 | call5(r.Context(), w, r) 57 | call5(context.Background(), w, r) 58 | } 59 | 60 | func call8() { 61 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 62 | call5(r.Context(), w, r) 63 | call5(context.Background(), w, r) // Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead 64 | 65 | call6(w, r) 66 | 67 | // call7 should be like `func call7(ctx context.Context, in bool, w http.ResponseWriter, r *http.Request)` 68 | call7(true, w, r) // Function `call7` should pass the context parameter 69 | }) 70 | } 71 | ``` 72 | 73 | ## Tips 74 | ### need break ctx inheritance 75 | eg: [issue](https://github.com/kkHAIKE/contextcheck/issues/2). 76 | 77 | ```go 78 | func call1(ctx context.Context) { 79 | ... 80 | 81 | newCtx, cancel := NoInheritCancel(ctx) 82 | defer cancel() 83 | 84 | call2(newCtx) 85 | ... 86 | } 87 | 88 | func call2(ctx context.Context) { 89 | ... 90 | } 91 | 92 | func NoInheritCancel(_ context.Context) (context.Context,context.CancelFunc) { 93 | return context.WithCancel(context.Background()) 94 | } 95 | ``` 96 | 97 | ### skip the check for the specified function 98 | To skip this linter in some false-positive cases, you can add // nolint: contextcheck to the function declaration's comment. 99 | 100 | ```go 101 | // nolint: contextcheck 102 | func call1() { 103 | doSomeThing(context.Background()) // add nolint will no issuss for that 104 | } 105 | 106 | func call2(ctx context.Context) { 107 | call1() 108 | } 109 | 110 | func call3() { 111 | call2(context.Background()) 112 | } 113 | ``` 114 | 115 | ### force the marking of a specified function as having a server-side http.Request parameter 116 | The default behavior is to mark `http.HandlerFunc` or any function that uses `r.Context()`. 117 | 118 | ```go 119 | // @contextcheck(req_has_ctx) 120 | func writeErr(w http.ResponseWriter, r *http.Request, err error) { 121 | doSomeThing(r.Context()) 122 | } 123 | 124 | func handler(w http.ResponseWriter, r *http.Request) { 125 | ... 126 | if err != nil { 127 | writeErr(w, r, err) 128 | return 129 | } 130 | ... 131 | } 132 | ``` 133 | 134 | ## Installation 135 | 136 | You can get `contextcheck` by `go get` command. 137 | 138 | ```bash 139 | $ go get -u github.com/kkHAIKE/contextcheck 140 | ``` 141 | 142 | or build yourself. 143 | 144 | ```bash 145 | $ make build 146 | $ make install 147 | ``` 148 | 149 | ## Usage 150 | 151 | Invoke `contextcheck` with your package name 152 | 153 | ```bash 154 | $ contextcheck ./... 155 | $ # or 156 | $ go vet -vettool=`which contextcheck` ./... 157 | ``` 158 | -------------------------------------------------------------------------------- /cmd/contextcheck/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kkHAIKE/contextcheck" 5 | "golang.org/x/tools/go/analysis/singlechecker" 6 | ) 7 | 8 | func main() { 9 | singlechecker.Main(contextcheck.NewAnalyzer(contextcheck.Configuration{})) 10 | } 11 | -------------------------------------------------------------------------------- /contextcheck.go: -------------------------------------------------------------------------------- 1 | package contextcheck 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gostaticanalysis/analysisutil" 12 | "golang.org/x/tools/go/analysis" 13 | "golang.org/x/tools/go/analysis/passes/buildssa" 14 | "golang.org/x/tools/go/packages" 15 | "golang.org/x/tools/go/ssa" 16 | ) 17 | 18 | type Configuration struct { 19 | DisableFact bool 20 | } 21 | 22 | var pkgprefix string 23 | 24 | func NewAnalyzer(cfg Configuration) *analysis.Analyzer { 25 | analyzer := &analysis.Analyzer{ 26 | Name: "contextcheck", 27 | Doc: "check whether the function uses a non-inherited context", 28 | Run: NewRun(nil, cfg.DisableFact), 29 | Requires: []*analysis.Analyzer{ 30 | buildssa.Analyzer, 31 | }, 32 | } 33 | analyzer.Flags.StringVar(&pkgprefix, "pkgprefix", "", "filter init pkgs (only for cmd)") 34 | 35 | if !cfg.DisableFact { 36 | analyzer.FactTypes = append(analyzer.FactTypes, (*ctxFact)(nil)) 37 | } 38 | 39 | return analyzer 40 | } 41 | 42 | const ( 43 | ctxPkg = "context" 44 | ctxName = "Context" 45 | 46 | httpPkg = "net/http" 47 | httpRes = "ResponseWriter" 48 | httpReq = "Request" 49 | ) 50 | 51 | const ( 52 | CtxIn int = 1 << iota // ctx in function's param 53 | CtxOut // ctx in function's results 54 | CtxInField // ctx in function's field param 55 | ) 56 | 57 | type entryType int 58 | 59 | const ( 60 | EntryNone entryType = iota 61 | EntryNormal // without ctx in 62 | EntryWithCtx // has ctx in 63 | EntryWithHttpHandler // is http handler 64 | ) 65 | 66 | var ( 67 | pkgFactMap = make(map[*types.Package]ctxFact) 68 | pkgFactMu sync.RWMutex 69 | ) 70 | 71 | type element interface { 72 | Pos() token.Pos 73 | Parent() *ssa.Function 74 | } 75 | 76 | type resInfo struct { 77 | Valid bool 78 | Funcs []string 79 | 80 | // reuse for doc 81 | ReqCtx bool 82 | Skip bool 83 | 84 | EntryType entryType 85 | } 86 | 87 | type ctxFact map[string]resInfo 88 | 89 | func (*ctxFact) String() string { return "ctxCheck" } 90 | func (*ctxFact) AFact() {} 91 | 92 | type runner struct { 93 | pass *analysis.Pass 94 | ctxTyp *types.Named 95 | ctxPTyp *types.Pointer 96 | skipFile map[*ast.File]bool 97 | 98 | httpResTyps []types.Type 99 | httpReqTyps []types.Type 100 | 101 | currentFact ctxFact 102 | disableFact bool 103 | } 104 | 105 | func getPkgRoot(pkg string) string { 106 | arr := strings.Split(pkg, "/") 107 | if len(arr) < 3 { 108 | return arr[0] 109 | } 110 | if strings.IndexByte(arr[0], '.') == -1 { 111 | return arr[0] 112 | } 113 | return strings.Join(arr[:3], "/") 114 | } 115 | 116 | func NewRun(pkgs []*packages.Package, disableFact bool) func(pass *analysis.Pass) (interface{}, error) { 117 | m := make(map[string]bool) 118 | for _, pkg := range pkgs { 119 | m[getPkgRoot(pkg.PkgPath)] = true 120 | } 121 | return func(pass *analysis.Pass) (interface{}, error) { 122 | // skip different repo 123 | if len(m) > 0 && !m[getPkgRoot(pass.Pkg.Path())] { 124 | return nil, nil 125 | } 126 | if len(m) == 0 && pkgprefix != "" && !strings.HasPrefix(pass.Pkg.Path(), pkgprefix) { 127 | return nil, nil 128 | } 129 | 130 | r := &runner{disableFact: disableFact} 131 | r.run(pass) 132 | return nil, nil 133 | } 134 | } 135 | 136 | func (r *runner) run(pass *analysis.Pass) { 137 | r.pass = pass 138 | pssa := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 139 | funcs := pssa.SrcFuncs 140 | 141 | // collect ctx obj 142 | var ok bool 143 | r.ctxTyp, r.ctxPTyp, ok = r.getRequiedType(pssa, ctxPkg, ctxName) 144 | if !ok { 145 | return 146 | } 147 | 148 | // collect http obj 149 | r.collectHttpTyps(pssa) 150 | 151 | r.skipFile = make(map[*ast.File]bool) 152 | r.currentFact = make(ctxFact) 153 | 154 | type entryInfo struct { 155 | f *ssa.Function // entryfunc 156 | tp entryType // entrytype 157 | } 158 | var tmpFuncs []entryInfo 159 | for _, f := range funcs { 160 | // skip checked function 161 | key := f.RelString(nil) 162 | if _, ok := r.currentFact[key]; ok { 163 | continue 164 | } 165 | 166 | if entryType := r.checkIsEntry(f); entryType == EntryNormal { 167 | if _, ok := r.getValue(key, f); ok { 168 | continue 169 | } 170 | // record the result of nomal function 171 | checkingMap := make(map[string]bool) 172 | checkingMap[key] = true 173 | r.setFact(key, r.checkFuncWithoutCtx(f, checkingMap), f.Name()) 174 | continue 175 | } else if entryType == EntryWithCtx || entryType == EntryWithHttpHandler { 176 | tmpFuncs = append(tmpFuncs, entryInfo{f: f, tp: entryType}) 177 | } 178 | } 179 | 180 | for _, v := range tmpFuncs { 181 | r.checkFuncWithCtx(v.f, v.tp) 182 | } 183 | 184 | if len(r.currentFact) > 0 { 185 | if r.disableFact { 186 | setPkgFact(pass.Pkg, r.currentFact) 187 | } else { 188 | pass.ExportPackageFact(&r.currentFact) 189 | } 190 | } 191 | } 192 | 193 | func (r *runner) getRequiedType(pssa *buildssa.SSA, path, name string) (obj *types.Named, pobj *types.Pointer, ok bool) { 194 | pkg := pssa.Pkg.Prog.ImportedPackage(path) 195 | if pkg == nil { 196 | return 197 | } 198 | 199 | objTyp := pkg.Type(name) 200 | if objTyp == nil { 201 | return 202 | } 203 | obj, ok = objTyp.Object().Type().(*types.Named) 204 | if !ok { 205 | return 206 | } 207 | pobj = types.NewPointer(obj) 208 | 209 | return 210 | } 211 | 212 | func (r *runner) collectHttpTyps(pssa *buildssa.SSA) { 213 | objRes, _, ok := r.getRequiedType(pssa, httpPkg, httpRes) 214 | if ok { 215 | r.httpResTyps = append(r.httpResTyps, objRes) 216 | } 217 | 218 | _, pobjReq, ok := r.getRequiedType(pssa, httpPkg, httpReq) 219 | if ok { 220 | r.httpReqTyps = append(r.httpReqTyps, pobjReq) 221 | } 222 | } 223 | 224 | func (r *runner) checkIsEntry(f *ssa.Function) (ret entryType) { 225 | // if r.noImportedContextAndHttp(f) { 226 | // return EntryNormal 227 | // } 228 | key := "entry:" + f.RelString(nil) 229 | res, ok := r.getValue(key, f) 230 | if ok { 231 | return res.EntryType 232 | } 233 | defer func() { 234 | r.currentFact[key] = resInfo{EntryType: ret} 235 | }() 236 | 237 | ctxIn, ctxOut := r.checkIsCtx(f) 238 | if ctxOut { 239 | // skip the function which generate ctx 240 | return EntryNone 241 | } else if ctxIn { 242 | // has ctx in, ignore *http.Request.Context() 243 | return EntryWithCtx 244 | } 245 | 246 | reqctx, skip := r.docFlag(f) 247 | 248 | // check is `func handler(w http.ResponseWriter, r *http.Request) {}` 249 | // or use '// @contextcheck(req_has_ctx)' 250 | if r.checkIsHttpHandler(f, reqctx) { 251 | return EntryWithHttpHandler 252 | } 253 | 254 | if skip { 255 | return EntryNone 256 | } 257 | 258 | return EntryNormal 259 | } 260 | 261 | func (r *runner) docFlag(f *ssa.Function) (reqctx, skip bool) { 262 | for _, v := range r.getDocFromFunc(f) { 263 | if len(nolintRe.FindString(v.Text)) > 0 && strings.Contains(v.Text, "contextcheck") { 264 | skip = true 265 | } else if strings.HasPrefix(v.Text, "// @contextcheck(req_has_ctx)") { 266 | reqctx = true 267 | } 268 | } 269 | return 270 | } 271 | 272 | var nolintRe = regexp.MustCompile(`^//\s?nolint:`) 273 | 274 | func (r *runner) getDocFromFunc(f *ssa.Function) []*ast.Comment { 275 | file := analysisutil.File(r.pass, f.Pos()) 276 | if file == nil { 277 | return nil 278 | } 279 | 280 | // only support FuncDecl comment 281 | var fd *ast.FuncDecl 282 | for _, v := range file.Decls { 283 | if tmp, ok := v.(*ast.FuncDecl); ok && tmp.Name.Pos() == f.Pos() { 284 | fd = tmp 285 | break 286 | } 287 | } 288 | if fd == nil || fd.Doc == nil || len(fd.Doc.List) == 0 { 289 | return nil 290 | } 291 | return fd.Doc.List 292 | } 293 | 294 | func (r *runner) checkIsCtx(f *ssa.Function) (in, out bool) { 295 | // check params 296 | tuple := f.Signature.Params() 297 | for i := 0; i < tuple.Len(); i++ { 298 | if r.isCtxType(tuple.At(i).Type()) { 299 | in = true 300 | break 301 | } 302 | } 303 | 304 | // check freevars 305 | for _, param := range f.FreeVars { 306 | if r.isCtxType(param.Type()) { 307 | in = true 308 | break 309 | } 310 | } 311 | 312 | // check results 313 | tuple = f.Signature.Results() 314 | for i := 0; i < tuple.Len(); i++ { 315 | if r.isCtxType(tuple.At(i).Type()) { 316 | out = true 317 | break 318 | } 319 | } 320 | return 321 | } 322 | 323 | func (r *runner) checkIsHttpHandler(f *ssa.Function, reqctx bool) bool { 324 | var hasReq bool 325 | tuple := f.Signature.Params() 326 | for i := 0; i < tuple.Len(); i++ { 327 | if r.isHttpReqType(tuple.At(i).Type()) { 328 | hasReq = true 329 | break 330 | } 331 | } 332 | if !hasReq { 333 | return false 334 | } 335 | if reqctx { 336 | return true 337 | } 338 | 339 | // must be `func f(w http.ResponseWriter, r *http.Request) {}` 340 | if f.Signature.Results().Len() == 0 && tuple.Len() == 2 && 341 | r.isHttpResType(tuple.At(0).Type()) && r.isHttpReqType(tuple.At(1).Type()) { 342 | return true 343 | } 344 | 345 | // check if use r.Context() 346 | return f.Blocks != nil && len(r.getHttpReqCtx(f, true)) > 0 347 | } 348 | 349 | func (r *runner) collectCtxRef(f *ssa.Function, isHttpHandler bool) (refMap map[ssa.Instruction]bool, ok bool) { 350 | ok = true 351 | refMap = make(map[ssa.Instruction]bool) 352 | checkedRefMap := make(map[ssa.Value]bool) 353 | storeInstrs := make(map[*ssa.Store]bool) 354 | phiInstrs := make(map[*ssa.Phi]bool) 355 | 356 | var checkRefs func(val ssa.Value, fromAddr bool) 357 | var checkInstr func(instr ssa.Instruction, fromAddr bool) 358 | 359 | checkRefs = func(val ssa.Value, fromAddr bool) { 360 | if val == nil || val.Referrers() == nil { 361 | return 362 | } 363 | 364 | if checkedRefMap[val] { 365 | return 366 | } 367 | checkedRefMap[val] = true 368 | 369 | for _, instr := range *val.Referrers() { 370 | checkInstr(instr, fromAddr) 371 | } 372 | } 373 | 374 | checkInstr = func(instr ssa.Instruction, fromAddr bool) { 375 | switch i := instr.(type) { 376 | case ssa.CallInstruction: 377 | refMap[i] = true 378 | tp := r.getCallInstrCtxType(i) 379 | if tp&CtxOut != 0 { 380 | // collect referrers of the results 381 | checkRefs(i.Value(), false) 382 | return 383 | } 384 | case *ssa.Store: 385 | if fromAddr { 386 | // collect all store to judge whether it's right value is valid 387 | storeInstrs[i] = true 388 | } else { 389 | checkRefs(i.Addr, true) 390 | } 391 | case *ssa.UnOp: 392 | checkRefs(i, false) 393 | case *ssa.MakeClosure: 394 | for _, param := range i.Bindings { 395 | if r.isCtxType(param.Type()) { 396 | refMap[i] = true 397 | break 398 | } 399 | } 400 | case *ssa.Extract: 401 | // only care about ctx 402 | if r.isCtxType(i.Type()) { 403 | checkRefs(i, false) 404 | } 405 | case *ssa.Phi: 406 | phiInstrs[i] = true 407 | checkRefs(i, false) 408 | case *ssa.TypeAssert: 409 | // ctx.(*bm.Context) 410 | } 411 | } 412 | 413 | if isHttpHandler { 414 | for _, v := range r.getHttpReqCtx(f, false) { 415 | checkRefs(v, false) 416 | } 417 | } else { 418 | for _, param := range f.Params { 419 | if r.isCtxType(param.Type()) { 420 | checkRefs(param, false) 421 | } 422 | } 423 | 424 | for _, param := range f.FreeVars { 425 | if r.isCtxType(param.Type()) { 426 | checkRefs(param, false) 427 | } 428 | } 429 | } 430 | 431 | for instr := range storeInstrs { 432 | if !checkedRefMap[instr.Val] { 433 | r.Reportf(instr, "Non-inherited new context, use function like `context.WithXXX` instead") 434 | ok = false 435 | } 436 | } 437 | 438 | for instr := range phiInstrs { 439 | for _, v := range instr.Edges { 440 | if !checkedRefMap[v] { 441 | r.Reportf(instr, "Non-inherited new context, use function like `context.WithXXX` instead") 442 | ok = false 443 | } 444 | } 445 | } 446 | 447 | return 448 | } 449 | 450 | func (r *runner) getHttpReqCtx(f *ssa.Function, least1 bool) (rets []ssa.Value) { 451 | checkedRefMap := make(map[ssa.Value]bool) 452 | 453 | var checkRefs func(val ssa.Value, fromAddr bool) 454 | var checkInstr func(instr ssa.Instruction, fromAddr bool) 455 | 456 | checkRefs = func(val ssa.Value, fromAddr bool) { 457 | if val == nil || val.Referrers() == nil { 458 | return 459 | } 460 | 461 | if checkedRefMap[val] { 462 | return 463 | } 464 | checkedRefMap[val] = true 465 | 466 | for _, instr := range *val.Referrers() { 467 | checkInstr(instr, fromAddr) 468 | } 469 | } 470 | 471 | checkInstr = func(instr ssa.Instruction, fromAddr bool) { 472 | switch i := instr.(type) { 473 | case ssa.CallInstruction: 474 | // r.Context() only has one recv 475 | if len(i.Common().Args) != 1 { 476 | break 477 | } 478 | 479 | // find r.Context() 480 | if r.getCallInstrCtxType(i)&CtxOut != CtxOut { 481 | break 482 | } 483 | 484 | // check is r.Context 485 | f := r.getFunction(instr) 486 | if f == nil || f.Name() != ctxName { 487 | break 488 | } 489 | if f.Signature.Recv() != nil { 490 | // collect the return of r.Context 491 | rets = append(rets, i.Value()) 492 | if least1 { 493 | return 494 | } 495 | } 496 | case *ssa.Store: 497 | if !fromAddr { 498 | checkRefs(i.Addr, true) 499 | } 500 | case *ssa.UnOp: 501 | checkRefs(i, false) 502 | case *ssa.Phi: 503 | checkRefs(i, false) 504 | case *ssa.MakeClosure: 505 | case *ssa.Extract: 506 | // http.Request can only be input 507 | } 508 | } 509 | 510 | for _, param := range f.Params { 511 | if r.isHttpReqType(param.Type()) { 512 | checkRefs(param, false) 513 | } 514 | } 515 | 516 | return 517 | } 518 | 519 | func (r *runner) checkFuncWithCtx(f *ssa.Function, tp entryType) { 520 | isHttpHandler := tp == EntryWithHttpHandler 521 | refMap, ok := r.collectCtxRef(f, isHttpHandler) 522 | if !ok { 523 | return 524 | } 525 | 526 | for _, b := range f.Blocks { 527 | for _, instr := range b.Instrs { 528 | tp, ok := r.getCtxType(instr) 529 | if !ok { 530 | continue 531 | } 532 | 533 | // checked in collectCtxRef, skipped 534 | if tp&CtxOut != 0 { 535 | continue 536 | } 537 | 538 | if tp&CtxIn != 0 { 539 | if !refMap[instr] { 540 | if isHttpHandler { 541 | r.Reportf(instr, "Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead") 542 | } else { 543 | r.Reportf(instr, "Non-inherited new context, use function like `context.WithXXX` instead") 544 | } 545 | } 546 | } 547 | 548 | ff := r.getFunction(instr) 549 | if ff == nil { 550 | continue 551 | } 552 | 553 | key := ff.RelString(nil) 554 | res, ok := r.getValue(key, ff) 555 | if ok && !res.Valid { 556 | if instr.Pos().IsValid() { 557 | r.Reportf(instr, "Function `%s` should pass the context parameter", strings.Join(reverse(res.Funcs), "->")) 558 | } else { 559 | r.Reportf(ff, "Function `%s` should pass the context parameter", strings.Join(reverse(res.Funcs), "->")) 560 | } 561 | } 562 | } 563 | } 564 | } 565 | 566 | func (r *runner) checkFuncWithoutCtx(f *ssa.Function, checkingMap map[string]bool) (ret bool) { 567 | ret = true 568 | orgKey := f.RelString(nil) 569 | var seted bool 570 | for _, b := range f.Blocks { 571 | for _, instr := range b.Instrs { 572 | tp, ok := r.getCtxType(instr) 573 | if !ok { 574 | continue 575 | } 576 | 577 | if tp&CtxOut != 0 { 578 | continue 579 | } 580 | 581 | // it is considered illegal as long as ctx is in the input and not in *struct X 582 | if tp&CtxIn != 0 { 583 | if tp&CtxInField == 0 { 584 | ret = false 585 | } 586 | } 587 | 588 | ff := r.getFunction(instr) 589 | if ff == nil { 590 | continue 591 | } 592 | 593 | key := ff.RelString(nil) 594 | res, ok := r.getValue(key, ff) 595 | if ok { 596 | if !res.Valid { 597 | ret = false 598 | 599 | // save the call link 600 | if !seted { 601 | seted = true 602 | r.setFact(orgKey, res.Valid, res.Funcs...) 603 | } 604 | } 605 | continue 606 | } 607 | 608 | // check is thunk or bound 609 | if strings.HasSuffix(key, "$thunk") || strings.HasSuffix(key, "$bound") { 610 | continue 611 | } 612 | 613 | if entryType := r.checkIsEntry(ff); entryType == EntryNormal { 614 | // cannot get info from fact, skip 615 | if ff.Blocks == nil { 616 | continue 617 | } 618 | 619 | // handler cycle call 620 | if checkingMap[key] { 621 | continue 622 | } 623 | checkingMap[key] = true 624 | 625 | valid := r.checkFuncWithoutCtx(ff, checkingMap) 626 | r.setFact(key, valid, ff.Name()) 627 | if res, ok := r.getValue(key, ff); ok && !valid && !seted { 628 | seted = true 629 | r.setFact(orgKey, valid, res.Funcs...) 630 | } 631 | if !valid { 632 | ret = false 633 | } 634 | } 635 | } 636 | } 637 | return ret 638 | } 639 | 640 | func (r *runner) getCtxType(instr ssa.Instruction) (tp int, ok bool) { 641 | switch i := instr.(type) { 642 | case ssa.CallInstruction: 643 | tp = r.getCallInstrCtxType(i) 644 | ok = true 645 | case *ssa.MakeClosure: 646 | tp = r.getMakeClosureCtxType(i) 647 | ok = true 648 | } 649 | return 650 | } 651 | 652 | func (r *runner) getCallInstrCtxType(c ssa.CallInstruction) (tp int) { 653 | // check params 654 | for _, v := range c.Common().Args { 655 | if r.isCtxType(v.Type()) { 656 | if vv, ok := v.(*ssa.UnOp); ok { 657 | if _, ok := vv.X.(*ssa.FieldAddr); ok { 658 | tp |= CtxInField 659 | } 660 | } 661 | 662 | tp |= CtxIn 663 | break 664 | } 665 | } 666 | 667 | // check results 668 | if v := c.Value(); v != nil { 669 | if r.isCtxType(v.Type()) { 670 | tp |= CtxOut 671 | } else { 672 | tuple, ok := v.Type().(*types.Tuple) 673 | if !ok { 674 | return 675 | } 676 | for i := 0; i < tuple.Len(); i++ { 677 | if r.isCtxType(tuple.At(i).Type()) { 678 | tp |= CtxOut 679 | break 680 | } 681 | } 682 | } 683 | } 684 | 685 | return 686 | } 687 | 688 | func (r *runner) getMakeClosureCtxType(c *ssa.MakeClosure) (tp int) { 689 | for _, v := range c.Bindings { 690 | if r.isCtxType(v.Type()) { 691 | if vv, ok := v.(*ssa.UnOp); ok { 692 | if _, ok := vv.X.(*ssa.FieldAddr); ok { 693 | tp |= CtxInField 694 | } 695 | } 696 | 697 | tp |= CtxIn 698 | break 699 | } 700 | } 701 | return 702 | } 703 | 704 | func (r *runner) getFunction(instr ssa.Instruction) (f *ssa.Function) { 705 | switch i := instr.(type) { 706 | case ssa.CallInstruction: 707 | if i.Common().IsInvoke() { 708 | return 709 | } 710 | 711 | switch c := i.Common().Value.(type) { 712 | case *ssa.Function: 713 | f = c 714 | case *ssa.MakeClosure: 715 | // captured in the outer layer 716 | case *ssa.Builtin, *ssa.UnOp, *ssa.Lookup, *ssa.Phi: 717 | // skipped 718 | case *ssa.Extract, *ssa.Call: 719 | // function is a result of a call, skipped 720 | case *ssa.Parameter: 721 | // function is a param, skipped 722 | } 723 | case *ssa.MakeClosure: 724 | f = i.Fn.(*ssa.Function) 725 | } 726 | return 727 | } 728 | 729 | func (r *runner) isCtxType(tp types.Type) bool { 730 | if p, ok := tp.(*types.Pointer); ok { 731 | // opaqueType is not exposed and lead to unreachable error. 732 | // Related to https://github.com/golang/tools/blob/63229bc79404d8cf2fe4e88ad569168fe251d993/go/ssa/builder.go#L107 733 | if p.Elem().String() == "deferStack" { 734 | return false 735 | } 736 | } 737 | 738 | return types.Identical(tp, r.ctxTyp) || types.Identical(tp, r.ctxPTyp) 739 | } 740 | 741 | func (r *runner) isHttpResType(tp types.Type) bool { 742 | for _, v := range r.httpResTyps { 743 | if ok := types.Identical(v, v); ok { 744 | return true 745 | } 746 | } 747 | return false 748 | } 749 | 750 | func (r *runner) isHttpReqType(tp types.Type) bool { 751 | for _, v := range r.httpReqTyps { 752 | if ok := types.Identical(tp, v); ok { 753 | return true 754 | } 755 | } 756 | return false 757 | } 758 | 759 | func (r *runner) getValue(key string, f *ssa.Function) (res resInfo, ok bool) { 760 | res, ok = r.currentFact[key] 761 | if ok { 762 | return 763 | } 764 | 765 | if f.Pkg == nil { 766 | return 767 | } 768 | 769 | var fact ctxFact 770 | var got bool 771 | if r.disableFact { 772 | fact, got = getPkgFact(f.Pkg.Pkg) 773 | } else { 774 | got = r.pass.ImportPackageFact(f.Pkg.Pkg, &fact) 775 | } 776 | if got { 777 | res, ok = fact[key] 778 | } 779 | return 780 | } 781 | 782 | func (r *runner) setFact(key string, valid bool, funcs ...string) { 783 | var names []string 784 | if !valid { 785 | names = append(r.currentFact[key].Funcs, funcs...) 786 | } 787 | r.currentFact[key] = resInfo{ 788 | Valid: valid, 789 | Funcs: names, 790 | } 791 | } 792 | 793 | func (r *runner) Reportf(instr element, format string, args ...interface{}) { 794 | pos := instr.Pos() 795 | 796 | if !pos.IsValid() && instr.Parent() != nil { 797 | pos = instr.Parent().Pos() 798 | } 799 | 800 | if !pos.IsValid() { 801 | return 802 | } 803 | 804 | r.pass.Reportf(pos, format, args...) 805 | } 806 | 807 | // setPkgFact save fact to mem 808 | func setPkgFact(pkg *types.Package, fact ctxFact) { 809 | pkgFactMu.Lock() 810 | pkgFactMap[pkg] = fact 811 | pkgFactMu.Unlock() 812 | } 813 | 814 | // getPkgFact get fact from mem 815 | func getPkgFact(pkg *types.Package) (fact ctxFact, ok bool) { 816 | pkgFactMu.RLock() 817 | fact, ok = pkgFactMap[pkg] 818 | pkgFactMu.RUnlock() 819 | return 820 | } 821 | 822 | func reverse(arr1 []string) (arr2 []string) { 823 | l := len(arr1) 824 | if l == 0 { 825 | return 826 | } 827 | arr2 = make([]string, l) 828 | for i := 0; i <= l/2; i++ { 829 | arr2[i] = arr1[l-1-i] 830 | arr2[l-1-i] = arr1[i] 831 | } 832 | return 833 | } 834 | -------------------------------------------------------------------------------- /contextcheck_test.go: -------------------------------------------------------------------------------- 1 | package contextcheck_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/kkHAIKE/contextcheck" 8 | "golang.org/x/tools/go/analysis/analysistest" 9 | "golang.org/x/tools/go/packages" 10 | ) 11 | 12 | func Test(t *testing.T) { 13 | log.SetFlags(log.Lshortfile) 14 | testdata := analysistest.TestData() 15 | analyzer := contextcheck.NewAnalyzer(contextcheck.Configuration{}) 16 | analyzer.Run = contextcheck.NewRun([]*packages.Package{{PkgPath: "a"}}, false) 17 | analysistest.Run(t, testdata, analyzer, "a") 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kkHAIKE/contextcheck 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gostaticanalysis/analysisutil v0.7.1 7 | golang.org/x/tools v0.30.0 8 | ) 9 | 10 | require ( 11 | github.com/gostaticanalysis/comment v1.5.0 // indirect 12 | golang.org/x/mod v0.23.0 // indirect 13 | golang.org/x/sync v0.11.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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 30 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 37 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 51 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 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 | -------------------------------------------------------------------------------- /testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a // want package:"ctxCheck" 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "slices" 7 | ) 8 | 9 | type MyString string 10 | 11 | type TestInterface interface { 12 | Test() string 13 | } 14 | 15 | type xx struct{} 16 | 17 | func newXX() TestInterface { 18 | return &xx{} 19 | } 20 | 21 | func (x xx) Test() string { 22 | return "" 23 | } 24 | 25 | type MyInt int 26 | 27 | func (x MyInt) F() int { 28 | return int(x) 29 | } 30 | 31 | func f1(ctx context.Context) { 32 | defer f2(ctx) 33 | go f2(ctx) 34 | f2(ctx) 35 | 36 | ctx = context.WithValue(ctx, MyString("aaa"), "aaaaaa") 37 | f2(ctx) 38 | 39 | newXX().Test() 40 | 41 | f3() // want "Function `f3` should pass the context parameter" 42 | f6() // want "Function `f6->f3` should pass the context parameter" 43 | 44 | defer func() { 45 | f2(ctx) 46 | }() 47 | 48 | func(ctx context.Context) { 49 | f2(ctx) 50 | }(ctx) 51 | 52 | f2(context.Background()) // want "Non-inherited new context, use function like `context.WithXXX` instead" 53 | 54 | thunk := MyInt.F 55 | thunk(0) 56 | 57 | bound := MyInt(0).F 58 | bound() 59 | } 60 | 61 | func f2(ctx context.Context) {} 62 | 63 | func f3() { 64 | f2(context.TODO()) 65 | } 66 | 67 | func f4(ctx context.Context) { 68 | f2(ctx) 69 | ctx = context.Background() 70 | f2(ctx) // want "Non-inherited new context, use function like `context.WithXXX` instead" 71 | } 72 | 73 | func f5(ctx context.Context) { 74 | func() { 75 | f2(ctx) 76 | }() 77 | 78 | ctx = context.Background() // want "Non-inherited new context, use function like `context.WithXXX` instead" 79 | f2(ctx) 80 | } 81 | 82 | func f6() { 83 | f3() 84 | } 85 | 86 | func f7(ctx context.Context) { 87 | ctx, cancel := getNewCtx(ctx) 88 | defer cancel() 89 | 90 | f2(ctx) // OK 91 | } 92 | 93 | func getNewCtx(ctx context.Context) (newCtx context.Context, cancel context.CancelFunc) { 94 | return context.WithCancel(ctx) 95 | } 96 | 97 | /* ----------------- http ----------------- */ 98 | 99 | func f8(ctx context.Context, w http.ResponseWriter, r *http.Request) { 100 | } 101 | 102 | func f9(w http.ResponseWriter, r *http.Request) { 103 | ctx := r.Context() 104 | f8(ctx, w, r) 105 | f8(context.Background(), w, r) // want "Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead" 106 | } 107 | 108 | func f10(in bool, w http.ResponseWriter, r *http.Request) { 109 | f8(r.Context(), w, r) 110 | f8(context.Background(), w, r) // want "Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead" 111 | } 112 | 113 | // nolint: contextcheck 114 | func f14(w http.ResponseWriter, r *http.Request, err error) { 115 | f8(context.Background(), w, r) 116 | } 117 | 118 | // @contextcheck(req_has_ctx) 119 | func f15(w http.ResponseWriter, r *http.Request, err error) { 120 | f8(r.Context(), w, r) 121 | } 122 | 123 | func f11() { 124 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 125 | f8(r.Context(), w, r) 126 | f8(context.Background(), w, r) // want "Non-inherited new context, use function like `context.WithXXX` or `r.Context` instead" 127 | 128 | f9(w, r) 129 | 130 | f10(true, w, r) 131 | f14(w, r, nil) 132 | f15(w, r, nil) 133 | }) 134 | } 135 | 136 | /* ----------------- generics ----------------- */ 137 | 138 | type MySlice[T int | float32] []T 139 | 140 | func (s MySlice[T]) f12(ctx context.Context) T { 141 | f3() // want "Function `f3` should pass the context parameter" 142 | 143 | var sum T 144 | for _, value := range s { 145 | sum += value 146 | } 147 | return sum 148 | } 149 | 150 | func f13[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](ctx context.Context, a, b T) T { 151 | f3() // want "Function `f3` should pass the context parameter" 152 | 153 | if a > b { 154 | return a 155 | } 156 | 157 | return b 158 | } 159 | 160 | /* ----------------- issue 21 ----------------- */ 161 | 162 | func f16(ctx context.Context, k string) func() { 163 | return func() { // want "Function `f16\\$1` should pass the context parameter" 164 | f16(context.Background(), k) 165 | } 166 | } 167 | 168 | func f17(ctx context.Context, k string) func() func() { 169 | return func() func() { // want "Function `f17\\$1->f17\\$1\\$1` should pass the context parameter" 170 | return func() { 171 | f16(context.Background(), k) 172 | } 173 | } 174 | } 175 | 176 | /* ----------------- range over iter.Seq ----------------- */ 177 | 178 | func fIterSeq() { 179 | seq := slices.Values([]any{"a"}) 180 | for range seq { 181 | _, cancel := context.WithCancel(context.Background()) 182 | defer cancel() 183 | } 184 | } 185 | --------------------------------------------------------------------------------