Compare commits

...

109 commits

Author SHA1 Message Date
dependabot[bot]
bc6bc802c2 build(deps): bump golangci/golangci-lint-action
Bumps the github-actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 6 to 7
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 19:09:31 +02:00
ecae7266e2 migrate to golangci-lint v2 and enable stricter linters 2025-04-02 19:07:19 +02:00
pre-commit-ci[bot]
134c7a8397 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.64.7 → v2.0.2](https://github.com/golangci/golangci-lint/compare/v1.64.7...v2.0.2)
2025-04-02 19:07:19 +02:00
e5c81ff0dd update minimum go version 2025-04-02 18:39:55 +02:00
dependabot[bot]
a3efb8903a build(deps): bump golang.org/x/tools from 0.30.0 to 0.31.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 18:39:55 +02:00
pre-commit-ci[bot]
96ab752057 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.64.6 → v1.64.7](https://github.com/golangci/golangci-lint/compare/v1.64.6...v1.64.7)
2025-03-17 23:05:55 +01:00
pre-commit-ci[bot]
2cfdadfd85 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.64.5 → v1.64.6](https://github.com/golangci/golangci-lint/compare/v1.64.5...v1.64.6)
2025-03-03 23:53:01 +01:00
dependabot[bot]
5a001f2fbe build(deps): bump golang.org/x/tools from 0.29.0 to 0.30.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.29.0 to 0.30.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-02 08:26:11 +01:00
pre-commit-ci[bot]
6f8b43553d [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.63.4 → v1.64.5](https://github.com/golangci/golangci-lint/compare/v1.63.4...v1.64.5)
2025-02-18 00:25:11 +01:00
dependabot[bot]
3947c6f8d5 build(deps): bump golang.org/x/tools from 0.28.0 to 0.29.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 01:09:26 +01:00
Fernandez Ludovic
b568e8acf1 chore: groups github action updates 2025-01-17 00:57:15 +01:00
Fernandez Ludovic
ed135bb8e7 chore: use stable and olstable instead of explicit versions 2025-01-17 00:57:15 +01:00
Fernandez Ludovic
12e5409dea tests: add tests on suggested fixes 2025-01-17 00:54:35 +01:00
Fernandez Ludovic
2046ce80db fix: cgo 2025-01-17 00:46:58 +01:00
Fernandez Ludovic
7b0afb1f92 tests: rewrite tests 2025-01-17 00:46:58 +01:00
f887074f5d refactor: nested context recursion 2025-01-17 00:02:58 +01:00
ef9d47d1f0 feat: better discriminate assignations to struct pointers 2025-01-17 00:02:58 +01:00
939d65bc16 fix(goreleaser): draft release 2025-01-14 17:33:18 +01:00
54e593c1c6 feat: ignore context.TODO and context.Background
Related to 
2025-01-14 17:05:35 +01:00
529e088561 fix: goreleaser v2 2025-01-13 22:58:40 +01:00
pre-commit-ci[bot]
52f7fb588c [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.62.2 → v1.63.4](https://github.com/golangci/golangci-lint/compare/v1.62.2...v1.63.4)
2025-01-07 07:49:41 +01:00
dependabot[bot]
6130ad946e build(deps): bump golang.org/x/tools from 0.27.0 to 0.28.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 22:41:48 +01:00
dependabot[bot]
48ee2433d6 build(deps): bump golang.org/x/tools from 0.26.0 to 0.27.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-01 23:53:16 +01:00
pre-commit-ci[bot]
4c828f7302 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.62.0 → v1.62.2](https://github.com/golangci/golangci-lint/compare/v1.62.0...v1.62.2)
2024-11-26 16:37:34 +01:00
Oleksandr Redko
632a706303 refactor: avoid one string to []byte conversion 2024-11-20 15:58:58 +01:00
Oleksandr Redko
98578576b8 chore: format with goimports 2024-11-20 15:56:39 +01:00
pre-commit-ci[bot]
4ad817c8f3 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.61.0 → v1.62.0](https://github.com/golangci/golangci-lint/compare/v1.61.0...v1.62.0)
2024-11-12 07:25:20 +01:00
dependabot[bot]
fae7a27f40 build(deps): bump golang.org/x/tools from 0.25.0 to 0.26.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-02 09:32:26 +01:00
Gabriel Augendre
fbf73fbd4d
update badge link to wiki 2024-10-30 16:37:48 +01:00
Gabriel Augendre
ed98e56f00
prevent concurrent coverage reports 2024-10-30 16:35:52 +01:00
Gabriel Augendre
7f2b12beab
add coverage badge 2024-10-30 16:29:06 +01:00
Gabriel Augendre
9b1b0c8986
remove unused contrib folder 2024-10-30 16:28:56 +01:00
Gabriel Augendre
1c05d23bb3
fix condition on coverage report job 2024-10-30 16:23:45 +01:00
Gabriel Augendre
e1e94fa7d4
use separate job with specific permissions for coverage report 2024-10-30 16:23:02 +01:00
pre-commit-ci[bot]
db40be2dee [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-10-30 16:17:27 +01:00
Gabriel Augendre
0c76b071d6 add coverage report 2024-10-30 16:17:27 +01:00
Gabriel Augendre
cddb074802
add badges in readme 2024-10-30 15:35:31 +01:00
pre-commit-ci[bot]
548d1beeac [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
2024-10-07 23:43:57 -04:00
dependabot[bot]
7e1e279ad5 build(deps): bump golang.org/x/tools from 0.24.0 to 0.25.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-02 13:53:46 +02:00
pre-commit-ci[bot]
88173f8b3f [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.60.3 → v1.61.0](https://github.com/golangci/golangci-lint/compare/v1.60.3...v1.61.0)
2024-09-10 07:13:57 +02:00
dependabot[bot]
4dc5fc8817 build(deps): bump golang.org/x/tools from 0.23.0 to 0.24.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-02 00:16:13 +02:00
Fernandez Ludovic
25eb4fa74b chore: ignore binary 2024-09-01 13:40:56 +02:00
Fernandez Ludovic
4526c31d1d fix: use mon go1.22.0 2024-09-01 13:40:56 +02:00
pre-commit-ci[bot]
8782199988 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.60.1 → v1.60.3](https://github.com/golangci/golangci-lint/compare/v1.60.1...v1.60.3)
2024-08-27 08:09:22 +02:00
Gabriel Augendre
4410b65005
doc: update linter description 2024-08-26 10:30:12 +02:00
90afb8d3fa update readme 2024-08-26 07:47:30 +02:00
021f1b4ed6 cd: use go1.23 for golangci-lint 2024-08-26 07:46:50 +02:00
Venkatesh Kotwade
be0aa70f23
Detect nested contexts in function literals ()
* feat: Add detection for nested contexts in function literals
* feat: Improve detection of nested contexts in function literals
* refactor: Update getReportMessage function to handle unsupported nested context types
* use node instead of block
* refactor: use multi case
* added one more case
* feat: also added support for multiple contexts
2024-08-26 07:46:02 +02:00
0d2c4019d4 update go.mod to 1.22 2024-08-25 18:15:22 +02:00
77afd24616 cd: always run all test jobs 2024-08-25 16:00:22 +02:00
2e1ec44b79 drop support for go 1.21, and test against released go 1.23 2024-08-25 15:59:59 +02:00
0be9888cea
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-08-20 08:05:44 +02:00
pre-commit-ci[bot]
ab55c271f9
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.59.1 → v1.60.1](https://github.com/golangci/golangci-lint/compare/v1.59.1...v1.60.1)
2024-08-19 22:52:08 +00:00
46a445090d
Merge pull request from Crocmagnon/dependabot/go_modules/golang.org/x/tools-0.23.0
build(deps): bump golang.org/x/tools from 0.22.0 to 0.23.0
2024-08-02 01:31:47 +02:00
dependabot[bot]
3df83da91f
build(deps): bump golang.org/x/tools from 0.22.0 to 0.23.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:38:51 +00:00
a80e8ddef6 cd: test with 1.23rc2 2024-07-23 19:13:11 +02:00
30606c7931
Merge pull request from Crocmagnon/feat/context-condition
handle contexts inside conditions
2024-07-23 19:07:45 +02:00
Gabriel Augendre
091030580e feat: detect in nested blocks 2024-07-23 19:07:34 +02:00
f35e8a2263
Merge pull request from Crocmagnon/go1.23
cd: test with go 1.23rc1
2024-07-02 00:32:37 +02:00
40f87bfc57 cd: test with go 1.23rc1 2024-07-02 00:30:31 +02:00
bf0cfec130
Merge pull request from Crocmagnon/dependabot/go_modules/golang.org/x/tools-0.22.0
build(deps): bump golang.org/x/tools from 0.21.0 to 0.22.0
2024-07-02 00:23:55 +02:00
c6325891c1
Merge pull request from Crocmagnon/dependabot/github_actions/goreleaser/goreleaser-action-6
build(deps): bump goreleaser/goreleaser-action from 5 to 6
2024-07-02 00:23:39 +02:00
dependabot[bot]
9a9e950a91
build(deps): bump goreleaser/goreleaser-action from 5 to 6
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 21:47:37 +00:00
dependabot[bot]
602eb047a9
build(deps): bump golang.org/x/tools from 0.21.0 to 0.22.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 21:23:29 +00:00
a2ca606f30
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-06-11 07:51:32 +02:00
pre-commit-ci[bot]
802ac1311c
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.59.0 → v1.59.1](https://github.com/golangci/golangci-lint/compare/v1.59.0...v1.59.1)
2024-06-10 22:55:26 +00:00
e8053df661
Merge pull request from MichaelUrman/contained
Test and fix cases with nested contexts
2024-05-29 17:29:50 +02:00
Michael Urman
69e9ae12fc Explain getRootIdent/ObjectOf/Pos check 2024-05-29 10:26:10 -05:00
Michael Urman
0e13d068ad Fix array test
The prior construct probably shouldn't issue an error.
These, however, both should (and do).
2024-05-29 10:07:03 -05:00
Michael Urman
3e9e29f41c Test and fix cases with nested contexts
As long as the context is rooted in a non-pointer value that has a new
copy in the loop, it is as safe to copy that value as it is to copy the
context. So only report such cases when they are indirected and thus
shared.
2024-05-29 09:47:58 -05:00
6be4ab74b8
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-28 07:23:56 +02:00
pre-commit-ci[bot]
db036b18d4
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.58.2 → v1.59.0](https://github.com/golangci/golangci-lint/compare/v1.58.2...v1.59.0)
2024-05-27 22:24:08 +00:00
9beb365772
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-21 08:56:02 +02:00
pre-commit-ci[bot]
954c9b64c6
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.58.1 → v1.58.2](https://github.com/golangci/golangci-lint/compare/v1.58.1...v1.58.2)
2024-05-20 21:54:23 +00:00
acb08d4731
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-14 07:20:53 +02:00
pre-commit-ci[bot]
66526fe956
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.58.0 → v1.58.1](https://github.com/golangci/golangci-lint/compare/v1.58.0...v1.58.1)
2024-05-13 22:24:40 +00:00
d5171e1d54
Merge pull request from Crocmagnon/dependabot/go_modules/golang.org/x/tools-0.21.0
build(deps): bump golang.org/x/tools from 0.19.0 to 0.21.0
2024-05-07 10:53:48 +02:00
dependabot[bot]
de95617005
build(deps): bump golang.org/x/tools from 0.19.0 to 0.21.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.19.0 to 0.21.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.19.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-07 08:50:53 +00:00
e7f3a75701
add dependabot config 2024-05-07 10:50:26 +02:00
ec963af14f
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-05-07 10:50:13 +02:00
da706a0193 update golangci-lint action 2024-05-07 10:45:21 +02:00
pre-commit-ci[bot]
0b0c70a0a6
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/golangci/golangci-lint: v1.57.2 → v1.58.0](https://github.com/golangci/golangci-lint/compare/v1.57.2...v1.58.0)
2024-05-06 22:31:22 +00:00
6a0a70a6a3
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-09 07:21:40 +02:00
pre-commit-ci[bot]
b6bd36f685
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
2024-04-08 23:04:56 +00:00
5cfbe16927
Update README.md 2024-04-06 09:09:43 +02:00
4c2ad68b8d
Merge pull request from Crocmagnon/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-02 08:08:43 +02:00
pre-commit-ci[bot]
a0fa7dbcdc
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v2.3.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v2.3.0...v4.5.0)
- [github.com/golangci/golangci-lint: v1.57.1 → v1.57.2](https://github.com/golangci/golangci-lint/compare/v1.57.1...v1.57.2)
2024-04-01 22:39:17 +00:00
a2187f17e7 fix: remove panic 2024-03-28 00:08:19 +01:00
95e882dd7d force github token in goreleaser 2024-03-28 00:01:25 +01:00
25cb4f05b9 fix goreleaser 2024-03-28 00:01:15 +01:00
781d685146 refactor: rename foreshadow to fatcontext 2024-03-27 23:50:12 +01:00
a7ef75e6e9 ci: skip go test in pre-commit.ci 2024-03-27 23:07:39 +01:00
da0fa3f168 Revert "ci: run go checks in pre-commit.ci"
This reverts commit 0ce2055c51.
2024-03-27 23:07:11 +01:00
0ce2055c51 ci: run go checks in pre-commit.ci 2024-03-27 23:06:08 +01:00
384f8008b9 docs: update & document dev commands 2024-03-27 22:59:32 +01:00
03bce0f06d ci: run goreleaser only if test & lint pass 2024-03-27 22:51:04 +01:00
48329d1b05 chore: setup goreleaser 2024-03-27 22:48:14 +01:00
8f5da5c4f4 run golangci-lint on ubuntu only 2024-03-27 22:35:57 +01:00
ad16182dc5 add .gitattributes to fix windows builds 2024-03-27 22:23:20 +01:00
92a12682a1 fix build job name 2024-03-27 22:18:59 +01:00
0f93f08dfd add write checks for golangci-lint action 2024-03-27 22:18:21 +01:00
01653ec5e6 build for matrix of os & go versions 2024-03-27 22:17:29 +01:00
31fa0cfa64 skip golangci-lint in pre-commit.ci 2024-03-27 22:12:57 +01:00
fe11a610f8 setup linter in github actions 2024-03-27 22:11:46 +01:00
08021fa72a Trigger pre-commit.ci 2024-03-27 22:04:44 +01:00
31dcab6d70 add linter and pre-commit 2024-03-27 22:01:27 +01:00
3aa8e86695
add LICENSE 2024-03-27 20:04:07 +01:00
89787d3472
add github actions 2024-03-27 20:01:10 +01:00
e294cd822f fix: don't touch AST 2024-03-27 19:57:23 +01:00
23 changed files with 1203 additions and 122 deletions

16
.github/dependabot.yml vendored Normal file
View file

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

49
.github/workflows/go.yml vendored Normal file
View file

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

30
.github/workflows/golangci-lint.yml vendored Normal file
View file

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

29
.github/workflows/goreleaser.yml vendored Normal file
View file

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

3
.gitignore vendored
View file

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

18
.golangci.yml Normal file
View file

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

44
.goreleaser.yaml Normal file
View file

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

21
.pre-commit-config.yaml Normal file
View file

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

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Gabriel Augendre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
Makefile Normal file
View file

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

View file

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

12
cmd/fatcontext/main.go Normal file
View file

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

View file

@ -1,10 +0,0 @@
package main
import (
"github.com/Crocmagnon/foreshadow/pkg/analyzer"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() {
singlechecker.Main(analyzer.Analyzer)
}

View file

@ -1,21 +0,0 @@
package contrib
import "context"
func ok() {
ctx := context.Background()
for i := 0; i < 10; i++ {
ctx := context.WithValue(ctx, "key", i)
_ = ctx
}
}
func notOk() {
ctx := context.Background()
for i := 0; i < 10; i++ {
ctx = context.WithValue(ctx, "key", i) // "context not shadowed in loop"
_ = ctx
}
}

11
go.mod
View file

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

14
go.sum
View file

@ -1,6 +1,8 @@
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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