diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index f868c842eaf9..e480a2e8b6ec 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -2316,6 +2316,15 @@ linters-settings: # Default: true begin: false + ttempdir: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + # The option `max-recursion-level` is uses to analyze function calls in recursion. + # Default: 5 + max-recursion-level: 10 + usestdlibvars: # Suggest the use of http.MethodXX. # Default: true @@ -2659,6 +2668,7 @@ linters: - testpackage - thelper - tparallel + - ttempdir - typecheck - unconvert - unparam @@ -2774,6 +2784,7 @@ linters: - testpackage - thelper - tparallel + - ttempdir - typecheck - unconvert - unparam diff --git a/go.mod b/go.mod index c9cab6fd994b..652f85596c62 100644 --- a/go.mod +++ b/go.mod @@ -83,6 +83,7 @@ require ( github.com/nishanths/exhaustive v0.12.0 github.com/nishanths/predeclared v0.2.2 github.com/nunnatsa/ginkgolinter v0.16.2 + github.com/peczenyj/ttempdir v0.4.1 github.com/pelletier/go-toml/v2 v2.2.2 github.com/polyfloyd/go-errorlint v1.5.2 github.com/quasilyte/go-ruleguard/dsl v0.3.22 @@ -193,7 +194,7 @@ require ( golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 011af4f75e75..8693835fc6c7 100644 --- a/go.sum +++ b/go.sum @@ -413,6 +413,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/peczenyj/ttempdir v0.4.1 h1:fOl4T3ugtb4Pinmzk+t3ShvBCQYIqF/juRTBZW69QXI= +github.com/peczenyj/ttempdir v0.4.1/go.mod h1:zK2cEgOq73piefMNWglvwqc7w6r4VTcAv2KJA2emj9k= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -804,8 +806,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index 0999c278efe7..a9e59560e34a 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -398,6 +398,7 @@ "testpackage", "thelper", "tparallel", + "ttempdir", "typecheck", "unconvert", "unparam", @@ -3137,6 +3138,22 @@ } } }, + "ttempdir": { + "type": "object", + "additionalProperties": false, + "properties": { + "all": { + "description": "The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.", + "type": "boolean", + "default": false + }, + "max-recursion-level": { + "description": "Max recursion level when checking nested arg calls", + "type": "integer", + "default": 5 + } + } + }, "usestdlibvars": { "type": "object", "additionalProperties": false, diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index f159db2a96d8..7d13452a17f6 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -159,6 +159,10 @@ var defaultLintersSettings = LintersSettings{ SkipRegexp: `(export|internal)_test\.go`, AllowPackages: []string{"main"}, }, + Ttempdir: TtempdirSettings{ + All: false, + MaxRecursionLevel: 5, + }, Unparam: UnparamSettings{ Algo: "cha", }, @@ -269,6 +273,7 @@ type LintersSettings struct { Testifylint TestifylintSettings Testpackage TestpackageSettings Thelper ThelperSettings + Ttempdir TtempdirSettings Unconvert UnconvertSettings Unparam UnparamSettings Unused UnusedSettings @@ -919,6 +924,11 @@ type TenvSettings struct { All bool `mapstructure:"all"` } +type TtempdirSettings struct { + All bool `mapstructure:"all"` + MaxRecursionLevel uint `mapstructure:"max-recursion-level"` +} + type UseStdlibVarsSettings struct { HTTPMethod bool `mapstructure:"http-method"` HTTPStatusCode bool `mapstructure:"http-status-code"` diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_all.go b/pkg/golinters/ttempdir/testdata/ttempdir_all.go new file mode 100644 index 000000000000..de60d74f4287 --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_all.go @@ -0,0 +1,59 @@ +//golangcitest:args -Ettempdir +//golangcitest:config_path testdata/ttempdir_all.yml +package testdata + +import ( + "os" + "testing" +) + +var ( + _, ee = os.MkdirTemp("a", "b") // never seen +) + +func setup() { + os.MkdirTemp("a", "b") // never seen + _, err := os.MkdirTemp("a", "b") // never seen + if err != nil { + _ = err + } + os.MkdirTemp("a", "b") // never seen +} + +func F(t *testing.T) { + setup() + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = err + } +} + +func BF(b *testing.B) { + TBF(b) + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = err + } +} + +func TBF(tb testing.TB) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = err + } +} + +func FF(f *testing.F) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = err + } +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_all.yml b/pkg/golinters/ttempdir/testdata/ttempdir_all.yml new file mode 100644 index 000000000000..200b4482a441 --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_all.yml @@ -0,0 +1,4 @@ +linters-settings: + ttempdir: + all: true + max-recursion-level: 5 diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_all_test.go b/pkg/golinters/ttempdir/testdata/ttempdir_all_test.go new file mode 100644 index 000000000000..d41db499597d --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_all_test.go @@ -0,0 +1,82 @@ +//golangcitest:args -Ettempdir +//golangcitest:config_path testdata/ttempdir_all.yml +package testdata + +import ( + "os" + "testing" +) + +var ( + _, e = os.MkdirTemp("a", "b") // never seen +) + +func testsetup() { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in testsetup" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in testsetup" + if err != nil { + _ = err + } + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in testsetup" +} + +func TestF(t *testing.T) { + testsetup() + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = err + } + t.Cleanup(func() { + _, _ = os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in anonymous function" + }) +} + +func BenchmarkF(b *testing.B) { + TB(b) + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = err + } +} + +func TB(tb testing.TB) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = err + } +} + +func FuzzF(f *testing.F) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = err + } +} + +func TestFunctionLiteral(t *testing.T) { + testsetup() + t.Run("test", func(t *testing.T) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + } + }) +} + +func TestEmpty(t *testing.T) { + t.Run("test", func(*testing.T) {}) +} + +func TestEmptyTB(t *testing.T) { + func(testing.TB) {}(t) +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path.go new file mode 100644 index 000000000000..f5544b99d51a --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path.go @@ -0,0 +1,54 @@ +//golangcitest:args -Ettempdir +//golangcitest:expected_exitcode 0 +package testdata + +import ( + "os" + "testing" +) + +var ( + dir = os.TempDir() // never seen +) + +func setup() { + os.TempDir() // never seen + dir := os.TempDir() // never seen + _ = dir + _ = os.TempDir() // never seen +} + +func F(t *testing.T) { + setup() + t.TempDir() // never seen + t.Log(t.TempDir()) // never seen + _ = t.TempDir() // never seen + if dir := t.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func BF(b *testing.B) { + TBF(b) + b.TempDir() // never seen + _ = b.TempDir() // never seen + if dir := b.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func TBF(tb testing.TB) { + tb.TempDir() // never seen + _ = tb.TempDir() // never seen + if dir := tb.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func FF(f *testing.F) { + f.TempDir() // never seen + _ = f.TempDir() // never seen + if dir := f.TempDir(); dir != "" { // never seen + _ = dir + } +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path_test.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path_test.go new file mode 100644 index 000000000000..047dc982e9f6 --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_happy_path_test.go @@ -0,0 +1,115 @@ +//golangcitest:args -Ettempdir +//golangcitest:expected_exitcode 0 +package testdata + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +var ( + tdir = os.TempDir() // never seen +) + +func testsetup() { + os.TempDir() // never seen + dir := os.TempDir() // never seen + _ = dir + _ = os.TempDir() // never seen +} + +func TestF(t *testing.T) { + testsetup() + t.TempDir() // never seen + _ = t.TempDir() // never seen + if dir = t.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func BenchmarkF(b *testing.B) { + TB(b) + b.TempDir() // never seen + _ = b.TempDir() // never seen + if dir = b.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func TB(tb testing.TB) { + tb.TempDir() // never seen + _ = tb.TempDir() // never seen + if dir = tb.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func FuzzF(f *testing.F) { + f.TempDir() // never seen + _ = f.TempDir() // never seen + if dir = f.TempDir(); dir != "" { // never seen + _ = dir + } +} + +func TestFunctionLiteral(t *testing.T) { + testsetup() + t.Run("test", func(t *testing.T) { + t.TempDir() // never seen + _ = t.TempDir() // never seen + if dir = t.TempDir(); dir != "" { // never seen + _ = dir + } + }) +} + +func TestEmpty(t *testing.T) { + t.Run("test", func(*testing.T) {}) +} + +func TestEmptyTB(t *testing.T) { + func(testing.TB) {}(t) +} + +func TestRecursive(t *testing.T) { + t.Log( // recursion level 1 + t.TempDir(), // never seen + ) + t.Log( // recursion level 1 + fmt.Sprintf("%s", // recursion level 2 + t.TempDir(), // never seen + ), + ) + t.Log( // recursion level 1 + filepath.Clean( // recursion level 2 + fmt.Sprintf("%s", // recursion level 3 + t.TempDir(), // never seen + ), + ), + ) + t.Log( // recursion level 1 + filepath.Join( // recursion level 2 + filepath.Clean( // recursion level 3 + fmt.Sprintf("%s", // recursion level 4 + t.TempDir(), // never seen + ), + ), + "test", + ), + ) + t.Log( // recursion level 1 + fmt.Sprintf("%s/foo-%d", // recursion level 2 + filepath.Join( // recursion level 3 + filepath.Clean( // recursion level 4 + fmt.Sprintf("%s", // recursion level 5 + os.TempDir(), // max recursion level reached. + ), + ), + "test", + ), + 1024, + ), + ) +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp.go new file mode 100644 index 000000000000..b2d2b94aa37e --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp.go @@ -0,0 +1,79 @@ +//golangcitest:args -Ettempdir +package testdata + +import ( + "os" + "testing" +) + +var ( + _, ee = os.MkdirTemp("a", "b") // never seen +) + +func setup() { + os.MkdirTemp("a", "b") // never seen + _, err := os.MkdirTemp("a", "b") // never seen + if err != nil { + _ = err + } + os.MkdirTemp("a", "b") // never seen +} + +func F(t *testing.T) { + setup() + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = err + } + + _ = func(t *testing.T) { + _ = t + _, _ = os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + } + + t.Cleanup(func() { + _, _ = os.MkdirTemp("a", "b") + }) +} + +func BF(b *testing.B) { + TBF(b) + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = err + } + + func(b *testing.B) { + _ = b + _, _ = os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in anonymous function" + }(b) +} + +func TBF(tb testing.TB) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = err + } + + defer func(tb testing.TB) { + _ = tb + _, _ = os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in anonymous function" + }(tb) +} + +func FF(f *testing.F) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = err + } + + defer os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp_test.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp_test.go new file mode 100644 index 000000000000..3b2ee8451a9c --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_mkdirtemp_test.go @@ -0,0 +1,107 @@ +//golangcitest:args -Ettempdir +package testdata + +import ( + "fmt" + "os" + "testing" +) + +var ( + _, e = os.MkdirTemp("a", "b") // never seen +) + +func testsetup() { + os.MkdirTemp("a", "b") // never seen + _, err := os.MkdirTemp("a", "b") // never seen + if err != nil { + _ = err + } + os.MkdirTemp("a", "b") // never seen +} + +func TestF(t *testing.T) { + testsetup() + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = err + } +} + +func BenchmarkF(b *testing.B) { + TB(b) + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = err + } +} + +func TB(tb testing.TB) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = err + } +} + +func FuzzF(f *testing.F) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = err + } +} + +func TestFunctionLiteral(t *testing.T) { + testsetup() + t.Run("test", func(t *testing.T) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + } + }) +} + +func TestEmpty(t *testing.T) { + t.Run("test", func(*testing.T) {}) +} + +func TestEmptyTB(t *testing.T) { + func(testing.TB) {}(t) +} + +func TestTDD(t *testing.T) { + for _, tt := range []struct { + name string + }{ + {"test"}, + } { + t.Run(tt.name, func(t *testing.T) { + os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _, err := os.MkdirTemp("a", "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + if _, err := os.MkdirTemp("a", "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = err + } + }) + } +} + +func TestLoop(t *testing.T) { + for i := 0; i < 3; i++ { + os.MkdirTemp(fmt.Sprintf("a%d", i), "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestLoop" + _, err := os.MkdirTemp(fmt.Sprintf("a%d", i), "b") // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestLoop" + _ = err + if _, err := os.MkdirTemp(fmt.Sprintf("a%d", i), "b"); err != nil { // want "os\\.MkdirTemp\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestLoop" + _ = err + } + } +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir.go new file mode 100644 index 000000000000..993cc73267b7 --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir.go @@ -0,0 +1,53 @@ +//golangcitest:args -Ettempdir +package testdata + +import ( + "os" + "testing" +) + +var ( + dir = os.TempDir() // never seen +) + +func setup() { + os.TempDir() // never seen + dir := os.TempDir() // never seen + _ = dir + _ = os.TempDir() // never seen +} + +func F(t *testing.T) { + setup() + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + t.Log(os.TempDir()) // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + if dir := os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in F" + _ = dir + } +} + +func BF(b *testing.B) { + TBF(b) + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + if dir := os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BF" + _ = dir + } +} + +func TBF(tb testing.TB) { + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + if dir := os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TBF" + _ = dir + } +} + +func FF(f *testing.F) { + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + if dir := os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FF" + _ = dir + } +} diff --git a/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir_test.go b/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir_test.go new file mode 100644 index 000000000000..7a1941d3abab --- /dev/null +++ b/pkg/golinters/ttempdir/testdata/ttempdir_default_tempdir_test.go @@ -0,0 +1,114 @@ +//golangcitest:args -Ettempdir +package testdata + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +var ( + tdir = os.TempDir() // never seen +) + +func testsetup() { + os.TempDir() // if -all = true, want "os\\.TempDir\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in testsetup" + dir := os.TempDir() // if -all = true, want "os\\.TempDir\\(\\) should be replaced by `testing\\.TempDir\\(\\)` in testsetup" + _ = dir + _ = os.TempDir() // if -all = true, "func setup is not using testing.TempDir" +} + +func TestF(t *testing.T) { + testsetup() + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + if dir = os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestF" + _ = dir + } +} + +func BenchmarkF(b *testing.B) { + TB(b) + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + if dir = os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `b\\.TempDir\\(\\)` in BenchmarkF" + _ = dir + } +} + +func TB(tb testing.TB) { + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + if dir = os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `tb\\.TempDir\\(\\)` in TB" + _ = dir + } +} + +func FuzzF(f *testing.F) { + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + if dir = os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `f\\.TempDir\\(\\)` in FuzzF" + _ = dir + } +} + +func TestFunctionLiteral(t *testing.T) { + testsetup() + t.Run("test", func(t *testing.T) { + os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = os.TempDir() // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + if dir = os.TempDir(); dir != "" { // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in anonymous function" + _ = dir + } + }) +} + +func TestEmpty(t *testing.T) { + t.Run("test", func(*testing.T) {}) +} + +func TestEmptyTB(t *testing.T) { + func(testing.TB) {}(t) +} + +func TestRecursive(t *testing.T) { + t.Log( // recursion level 1 + os.TempDir(), // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestRecursive" + ) + t.Log( // recursion level 1 + fmt.Sprintf("%s", // recursion level 2 + os.TempDir(), // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestRecursive" + ), + ) + t.Log( // recursion level 1 + filepath.Clean( // recursion level 2 + fmt.Sprintf("%s", // recursion level 3 + os.TempDir(), // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestRecursive" + ), + ), + ) + t.Log( // recursion level 1 + filepath.Join( // recursion level 2 + filepath.Clean( // recursion level 3 + fmt.Sprintf("%s", // recursion level 4 + os.TempDir(), // want "os\\.TempDir\\(\\) should be replaced by `t\\.TempDir\\(\\)` in TestRecursive" + ), + ), + "test", + ), + ) + t.Log( // recursion level 1 + fmt.Sprintf("%s/foo-%d", // recursion level 2 + filepath.Join( // recursion level 3 + filepath.Clean( // recursion level 4 + fmt.Sprintf("%s", // recursion level 5 + os.TempDir(), // max recursion level reached. + ), + ), + "test", + ), + 1024, + ), + ) +} diff --git a/pkg/golinters/ttempdir/ttempdir.go b/pkg/golinters/ttempdir/ttempdir.go new file mode 100644 index 000000000000..0df141d0b385 --- /dev/null +++ b/pkg/golinters/ttempdir/ttempdir.go @@ -0,0 +1,30 @@ +package ttempdir + +import ( + "github.com/peczenyj/ttempdir/analyzer" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/goanalysis" +) + +func New(settings *config.TtempdirSettings) *goanalysis.Linter { + a := analyzer.New() + + var cfg map[string]map[string]any + if settings != nil { + cfg = map[string]map[string]any{ + a.Name: { + analyzer.FlagAllName: settings.All, + analyzer.FlagMaxRecursionLevelName: settings.MaxRecursionLevel, + }, + } + } + + return goanalysis.NewLinter( + a.Name, + "Detects the use of os.MkdirTemp, ioutil.TempDir or os.TempDir instead of t.TempDir", + []*analysis.Analyzer{a}, + cfg, + ).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/golinters/ttempdir/ttempdir_integration_test.go b/pkg/golinters/ttempdir/ttempdir_integration_test.go new file mode 100644 index 000000000000..590790e8a2fd --- /dev/null +++ b/pkg/golinters/ttempdir/ttempdir_integration_test.go @@ -0,0 +1,11 @@ +package ttempdir + +import ( + "testing" + + "github.com/golangci/golangci-lint/test/testshared/integration" +) + +func TestFromTestdata(t *testing.T) { + integration.RunTestdata(t) +} diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index a66f2eea0933..960647b9bad2 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -100,6 +100,7 @@ import ( "github.com/golangci/golangci-lint/pkg/golinters/testpackage" "github.com/golangci/golangci-lint/pkg/golinters/thelper" "github.com/golangci/golangci-lint/pkg/golinters/tparallel" + "github.com/golangci/golangci-lint/pkg/golinters/ttempdir" "github.com/golangci/golangci-lint/pkg/golinters/unconvert" "github.com/golangci/golangci-lint/pkg/golinters/unparam" "github.com/golangci/golangci-lint/pkg/golinters/unused" @@ -757,6 +758,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithLoadForGoAnalysis(). WithURL("https://github.com/moricho/tparallel"), + linter.NewConfig(ttempdir.New(&cfg.LintersSettings.Ttempdir)). + WithSince("v1.60.0"). + WithPresets(linter.PresetTest). + WithURL("https://github.com/peczenyj/ttempdir"), + linter.NewConfig(golinters.NewTypecheck()). WithInternal(). WithEnabledByDefault().