diff --git a/buildbuddy.yaml b/buildbuddy.yaml index c6a811d..0e2559b 100644 --- a/buildbuddy.yaml +++ b/buildbuddy.yaml @@ -9,3 +9,15 @@ actions: - "*" bazel_commands: - "test --config=workflows //..." + + - name: "Test example/staticcheck targets" + triggers: + push: + branches: + - "master" + pull_request: + branches: + - "*" + bazel_workspace_dir: "examples/staticcheck" + bazel_commands: + - "test --config=workflows //tests/..." diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel new file mode 100644 index 0000000..e69de29 diff --git a/examples/staticcheck/.bazelrc b/examples/staticcheck/.bazelrc new file mode 100644 index 0000000..f1698b7 --- /dev/null +++ b/examples/staticcheck/.bazelrc @@ -0,0 +1,13 @@ +# Copied part from the repo root. +# +test --test_output=errors +test --test_verbose_timeout_warnings +# BuildBuddy config +build:workflows --config=buildbuddy_bes_backend +build:workflows --config=buildbuddy_bes_results_url +build:workflows --config=buildbuddy_remote_cache +## TODO: remove this once BuildBuddy folks confirmed that +## the bug has been fixed on their end regarding command is +## not publicly displayed. +build:workflows --build_metadata=VISIBILITY=PUBLIC +test:workflows --keep_going diff --git a/examples/staticcheck/.gitignore b/examples/staticcheck/.gitignore new file mode 100644 index 0000000..ac51a05 --- /dev/null +++ b/examples/staticcheck/.gitignore @@ -0,0 +1 @@ +bazel-* diff --git a/examples/staticcheck/BUILD.bazel b/examples/staticcheck/BUILD.bazel new file mode 100644 index 0000000..ca4c9e0 --- /dev/null +++ b/examples/staticcheck/BUILD.bazel @@ -0,0 +1,32 @@ +load("@bazel_gazelle//:def.bzl", "gazelle") +load("@com_github_sluongng_nogo_analyzer//staticcheck:def.bzl", "staticcheck_analyzers") +load("@com_github_sluongng_nogo_analyzer//:def.bzl", "nogo_config") +load("@io_bazel_rules_go//go:def.bzl", "TOOLS_NOGO", "nogo") +load(":staticcheck.bzl", "STATICCHECK_ANALYZERS", "STATICCHECK_OVERRIDE") + +# gazelle:prefix github.com/sluongng/nogo-analyzer/examples/staticcheck +gazelle(name = "gazelle") + +gazelle( + name = "deps", + args = [ + "-from_file=go.mod", + "-to_macro=go_deps.bzl%go_deps", + "-prune", + ], + command = "update-repos", +) + +nogo_config( + name = "nogo_config", + out = "nogo_config.json", + analyzers = STATICCHECK_ANALYZERS, + override = STATICCHECK_OVERRIDE, +) + +nogo( + name = "nogo", + config = ":nogo_config.json", + visibility = ["//visibility:public"], + deps = staticcheck_analyzers(STATICCHECK_ANALYZERS), +) diff --git a/examples/staticcheck/README.md b/examples/staticcheck/README.md new file mode 100644 index 0000000..7d8efc9 --- /dev/null +++ b/examples/staticcheck/README.md @@ -0,0 +1,14 @@ +# Nogo/staticcheck example. + +This example setup includes initial nogo configuration with sevral checks and +check overrides. + +Targets: +* `bazel build //cmd/pass` built without errors. +* `bazel build //cmd/failed` fails with the following errors: + +``` +compilepkg: nogo: errors found by nogo during build-time code analysis: +cmd/failed/main.go:8:20: parsing time "01-01-2023" as "01-01-2023": cannot parse "" as "3" (SA1002) +cmd/failed/main.go:10:14: sleeping for 1 nanoseconds is probably a bug; be explicit if it isn't (SA1004) +``` diff --git a/examples/staticcheck/WORKSPACE b/examples/staticcheck/WORKSPACE new file mode 100644 index 0000000..15029db --- /dev/null +++ b/examples/staticcheck/WORKSPACE @@ -0,0 +1,56 @@ +workspace(name = "examples_staticcheck") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# http_archive( +# name = "com_github_sluongng_nogo_analyzer", +# sha256 = "a74a5e44751d292d17bd879e5aa8b40baa94b5dc2f043df1e3acbb3e23ead073", +# strip_prefix = "nogo-analyzer-0.0.2", +# urls = [ +# "https://github.com/sluongng/nogo-analyzer/archive/refs/tags/v0.0.2.tar.gz", +# ], +# ) + +# for dev usage only, see an example above for external usage. +local_repository( + name = "com_github_sluongng_nogo_analyzer", + path = "../..", +) + +http_archive( + name = "io_bazel_rules_go", + sha256 = "91585017debb61982f7054c9688857a2ad1fd823fc3f9cb05048b0025c47d023", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip", + ], +) + +load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") + +go_rules_dependencies() + +go_register_toolchains( + nogo = "@//:nogo", + version = "1.21.4", +) + +http_archive( + name = "bazel_gazelle", + sha256 = "b7387f72efb59f876e4daae42f1d3912d0d45563eac7cb23d1de0b094ab588cf", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", + ], +) + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") +load("//:go_deps.bzl", "go_deps") + +# gazelle:repository_macro go_deps.bzl%go_deps +go_deps() + +load("@com_github_sluongng_nogo_analyzer//staticcheck:deps.bzl", "staticcheck") + +staticcheck() + +gazelle_dependencies() diff --git a/examples/staticcheck/cmd/failed/BUILD.bazel b/examples/staticcheck/cmd/failed/BUILD.bazel new file mode 100644 index 0000000..a0804cf --- /dev/null +++ b/examples/staticcheck/cmd/failed/BUILD.bazel @@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +# fails with an error: +# +# compilepkg: nogo: errors found by nogo during build-time code analysis: +# cmd/failed/main.go:8:20: parsing time "01-01-2023" as "01-01-2023": cannot parse "" as "3" (SA1002) +# cmd/failed/main.go:10:14: sleeping for 1 nanoseconds is probably a bug; be explicit if it isn't (SA1004) +# +# Check SA4013 is excluded +go_library( + name = "failed_lib", + srcs = ["main.go"], + importpath = "github.com/sluongng/nogo-analyzer/examples/staticcheck/cmd/failed", + visibility = ["//visibility:private"], +) + +go_binary( + name = "failed", + embed = [":failed_lib"], + visibility = ["//visibility:public"], +) diff --git a/examples/staticcheck/cmd/failed/main.go b/examples/staticcheck/cmd/failed/main.go new file mode 100644 index 0000000..7d4b5da --- /dev/null +++ b/examples/staticcheck/cmd/failed/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "time" +) + +func main() { + _, _ = time.Parse("01-01-2023", "2023-01-01") + if !!true { + time.Sleep(1) + } +} diff --git a/examples/staticcheck/cmd/pass/BUILD.bazel b/examples/staticcheck/cmd/pass/BUILD.bazel new file mode 100644 index 0000000..5529254 --- /dev/null +++ b/examples/staticcheck/cmd/pass/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "pass_lib", + srcs = ["main.go"], + importpath = "github.com/sluongng/nogo-analyzer/examples/staticcheck/cmd/pass", + visibility = ["//visibility:private"], +) + +go_binary( + name = "pass", + embed = [":pass_lib"], + visibility = ["//visibility:public"], +) diff --git a/examples/staticcheck/cmd/pass/main.go b/examples/staticcheck/cmd/pass/main.go new file mode 100644 index 0000000..ffb6147 --- /dev/null +++ b/examples/staticcheck/cmd/pass/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + d, err := time.Parse("01-02-2006", "01-01-2023") + if err != nil { + panic(err) + } + + fmt.Printf("date: %s\n", d) +} diff --git a/examples/staticcheck/go_deps.bzl b/examples/staticcheck/go_deps.bzl new file mode 100644 index 0000000..0967ece --- /dev/null +++ b/examples/staticcheck/go_deps.bzl @@ -0,0 +1,4 @@ +load("@bazel_gazelle//:deps.bzl", "go_repository") + +def go_deps(): + pass diff --git a/examples/staticcheck/staticcheck.bzl b/examples/staticcheck/staticcheck.bzl new file mode 100644 index 0000000..e4be3d4 --- /dev/null +++ b/examples/staticcheck/staticcheck.bzl @@ -0,0 +1,18 @@ +SA1 = [ + "SA1002", # Invalid format in time.Parse + "SA1004", # Suspiciously small untyped constant in time.Sleep +] + +SA4 = [ + "SA4013", # Negating a boolean twice (!!b) is the same as writing b. This is either redundant, or a typo. +] + +STATICCHECK_ANALYZERS = SA1 + SA4 + +STATICCHECK_OVERRIDE = { + "SA4013": { + "exclude_files": { + "/": "excluded", + } + }, +} diff --git a/examples/staticcheck/tests/BUILD.bazel b/examples/staticcheck/tests/BUILD.bazel new file mode 100644 index 0000000..8cff233 --- /dev/null +++ b/examples/staticcheck/tests/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "tests", + size = "medium", + srcs = ["staticcheck_test.go"], + rule_files = [ + "@com_github_sluongng_nogo_analyzer//:all_files", + "@io_bazel_rules_go//:all_files", + "@bazel_gazelle//:all_files", + ], +) + +filegroup( + name = "all_files", + testonly = True, + srcs = [ + "BUILD.bazel", + "staticcheck_test.go", + ], + visibility = ["//visibility:public"], +) diff --git a/examples/staticcheck/tests/staticcheck_test.go b/examples/staticcheck/tests/staticcheck_test.go new file mode 100644 index 0000000..4a4612a --- /dev/null +++ b/examples/staticcheck/tests/staticcheck_test.go @@ -0,0 +1,214 @@ +package staticcheck_test + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strings" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Main: ` +-- BUILD.bazel -- +load("@bazel_gazelle//:def.bzl", "gazelle") +load("@com_github_sluongng_nogo_analyzer//staticcheck:def.bzl", "staticcheck_analyzers") +load("@com_github_sluongng_nogo_analyzer//:def.bzl", "nogo_config") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") + +# gazelle:prefix github.com/sluongng/nogo-analyzer/examples/staticcheck +gazelle(name = "gazelle") + +gazelle( + name = "deps", + args = [ + "-from_file=go.mod", + "-to_macro=go_deps.bzl%%go_deps", + "-prune", + ], + command = "update-repos", +) + +SA1 = [ + "SA1002", # Invalid format in time.Parse + "SA1004", # Suspiciously small untyped constant in time.Sleep +] + +SA4 = [ + "SA4013", # Negating a boolean twice (!!b) is the same as writing b. This is either redundant, or a typo. +] + +STATICCHECK_ANALYZERS = SA1 + SA4 + +STATICCHECK_OVERRIDE = { + "SA4013": { + "exclude_files": { + "/": "excluded", + } + }, +} + +nogo_config( + name = "nogo_config", + out = "nogo_config.json", + analyzers = STATICCHECK_ANALYZERS, + override = STATICCHECK_OVERRIDE, +) + +nogo( + name = "nogo", + config = ":nogo_config.json", + visibility = ["//visibility:public"], + deps = staticcheck_analyzers(STATICCHECK_ANALYZERS), +) + +go_library( + name = "failed", + srcs = ["failed.go"], + importpath = "failed", +) + +go_library( + name = "ok", + srcs = ["ok.go"], + importpath = "ok", +) + +-- failed.go -- +package main + +import "time" + +func main() { + _, _ = time.Parse("01-01-2023", "2023-01-01") + if !!true { + time.Sleep(1) + } +} + +-- ok.go -- +package main + +import ( + "fmt" + "time" +) + +func main() { + d, err := time.Parse("01-02-2006", "01-01-2023") + if err != nil { + panic(err) + } + + fmt.Printf("date: %%s\n", d) +} +`, + WorkspaceSuffix: ` +load("@com_github_sluongng_nogo_analyzer//staticcheck:deps.bzl", "staticcheck") + +staticcheck() + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") + +gazelle_dependencies() +`, + }) +} + +func Test(t *testing.T) { + for _, test := range []struct { + desc, nogo, target string + wantSuccess bool + analyzers, includes, excludes []string + }{ + { + desc: "fails for some checks", + nogo: "@//:nogo", + analyzers: []string{"ST1002", "ST1004", "SA4013"}, + target: "//:failed", + wantSuccess: false, + includes: []string{ + "compilepkg: nogo: errors found by nogo during build-time code analysis:", + `failed.go:6:20: parsing time "01-01-2023" as "01-01-2023": cannot parse "" as "3" \(SA1002\)`, + `failed.go:8:14: sleeping for 1 nanoseconds is probably a bug; be explicit if it isn't \(SA1004\)`, + }, + }, { + desc: "pass", + nogo: "@//:nogo", + analyzers: []string{"ST1002", "ST1004", "SA4013"}, + target: "//:ok", + wantSuccess: true, + }, + } { + t.Run(test.desc, func(t *testing.T) { + // ensure nogo is configured + if test.nogo != "" { + origRegister := "go_register_toolchains()" + customRegister := fmt.Sprintf("go_register_toolchains(nogo = %q)", test.nogo) + if err := replaceInFile("WORKSPACE", origRegister, customRegister, false); err != nil { + t.Fatal(err) + } + defer replaceInFile("WORKSPACE", customRegister, origRegister, false) + } + + // ensure staticcheck analyzer is configured in nogo + if len(test.analyzers) == 0 { + t.Fatal("enabling nogo requires at least one analyzer configured") + } + analyzerStr := strings.Join(test.analyzers, `", "`) + if err := replaceInFile("BUILD.bazel", "NOGO_ANALYZER_PLACEHOLDER", analyzerStr, true); err != nil { + t.Fatal(err) + } + + // run bazel build + cmd := bazel_testing.BazelCmd("build", test.target) + stderr := &bytes.Buffer{} + cmd.Stderr = stderr + if err := cmd.Run(); err == nil && !test.wantSuccess { + t.Fatal("unexpected success") + } else if err != nil && test.wantSuccess { + t.Logf("output: %s\n", stderr.Bytes()) + t.Fatalf("unexpected error: %v", err) + } + t.Logf("output: %s\n", stderr.Bytes()) + + // check content of stderr + for _, pattern := range test.includes { + if matched, err := regexp.Match(pattern, stderr.Bytes()); err != nil { + t.Fatal(err) + } else if !matched { + t.Errorf("output did not contain pattern: %s\n", pattern) + } + } + for _, pattern := range test.excludes { + if matched, err := regexp.Match(pattern, stderr.Bytes()); err != nil { + t.Fatal(err) + } else if matched { + t.Errorf("output contained pattern: %s", pattern) + } + } + + // return the BUILD.bazel nogo to original template for next test + if err := replaceInFile("BUILD.bazel", analyzerStr, "NOGO_ANALYZER_PLACEHOLDER", true); err != nil { + t.Fatal(err) + } + }) + } +} + +func replaceInFile(path, old, new string, once bool) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + if once { + data = bytes.Replace(data, []byte(old), []byte(new), 1) + } else { + data = bytes.ReplaceAll(data, []byte(old), []byte(new)) + } + return os.WriteFile(path, data, 0o666) +}