diff options
Diffstat (limited to 'tests/core/nogo')
31 files changed, 2112 insertions, 0 deletions
diff --git a/tests/core/nogo/BUILD.bazel b/tests/core/nogo/BUILD.bazel new file mode 100644 index 00000000..46c7ee89 --- /dev/null +++ b/tests/core/nogo/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "rules_go_deps", + srcs = [":common.bzl"], + visibility = ["//visibility:public"], +) diff --git a/tests/core/nogo/README.rst b/tests/core/nogo/README.rst new file mode 100644 index 00000000..06b4e12a --- /dev/null +++ b/tests/core/nogo/README.rst @@ -0,0 +1,18 @@ +Core Go rules tests +=================== + +This contains tests of the nogo build-time code analysis tool. + +Contents +-------- + +.. Child list start + +* `Vet check <vet/README.rst>`_ +* `Nogo configuration <config/README.rst>`_ +* `nogo analyzers with dependencies <deps/README.rst>`_ +* `Custom nogo analyzers <custom/README.rst>`_ +* `nogo test with coverage <coverage/README.rst>`_ + +.. Child list end + diff --git a/tests/core/nogo/common.bzl b/tests/core/nogo/common.bzl new file mode 100644 index 00000000..14089ce2 --- /dev/null +++ b/tests/core/nogo/common.bzl @@ -0,0 +1,38 @@ +# Macros used by all nogo integration tests. + +BUILD_FAILED_TMPL = """ +if [[ result -eq 0 ]]; then + echo "TEST FAILED: expected build error" >&2 + result=1 +else + result=0 + {check_err} +fi +""" + +BUILD_PASSED_TMPL = """ +if [[ result -ne 0 ]]; then + echo "TEST FAILED: unexpected build error" >&2 + result=1 +else + {check_err} +fi +""" + +CONTAINS_ERR_TMPL = """ + lines=$(grep '{err}' bazel-output.txt | wc -l) + if [ $lines -eq 0 ]; then + echo "TEST FAILED: expected error message containing: '{err}'" >&2 + result=1 + elif [ $lines -ne 1 ]; then + echo "TEST FAILED: expected error message '{err}' appears more than once" >&2 + result=1 + fi +""" + +DOES_NOT_CONTAIN_ERR_TMPL = """ + if grep -q '{err}' bazel-output.txt; then + echo "TEST FAILED: received error message containing: '{err}'" >&2 + result=1 + fi +""" diff --git a/tests/core/nogo/config/BUILD.bazel b/tests/core/nogo/config/BUILD.bazel new file mode 100644 index 00000000..ce5ad406 --- /dev/null +++ b/tests/core/nogo/config/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "config_test", + srcs = ["config_test.go"], +) diff --git a/tests/core/nogo/config/README.rst b/tests/core/nogo/config/README.rst new file mode 100644 index 00000000..bcd4b730 --- /dev/null +++ b/tests/core/nogo/config/README.rst @@ -0,0 +1,17 @@ +Nogo configuration +================== + +.. _nogo: /go/nogo.rst +.. _go_binary: /docs/go/core/rules.md#_go_binary +.. _#1850: https://github.com/bazelbuild/rules_go/issues/1850 +.. _#2470: https://github.com/bazelbuild/rules_go/issues/2470 + +Tests that verify nogo_ works on targets compiled in non-default configurations. + +.. contents:: + +config_test +----------- + +Verifies that a `go_binary`_ can be built in non-default configurations with +nogo. Verifies `#1850`_, `#2470`_. diff --git a/tests/core/nogo/config/config_test.go b/tests/core/nogo/config/config_test.go new file mode 100644 index 00000000..3758d438 --- /dev/null +++ b/tests/core/nogo/config/config_test.go @@ -0,0 +1,62 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config_test + +import ( + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Nogo: "@io_bazel_rules_go//:tools_nogo", + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_binary( + name = "shared_bin", + srcs = ["shared_bin.go"], + linkmode = "c-shared", + deps = [":shared_lib"], +) + +go_library( + name = "shared_lib", + srcs = ["shared_lib.go"], + importpath = "example.com/nogo/config/shared_lib", +) + +-- shared_bin.go -- +package main + +import _ "example.com/nogo/config/shared_lib" + +func main() { +} + +-- shared_lib.go -- +package shared_lib + +`, + }) +} + +func TestShared(t *testing.T) { + if err := bazel_testing.RunBazel("build", "//:shared_bin"); err != nil { + t.Fatal(err) + } +} diff --git a/tests/core/nogo/coverage/BUILD.bazel b/tests/core/nogo/coverage/BUILD.bazel new file mode 100644 index 00000000..b14fcaf4 --- /dev/null +++ b/tests/core/nogo/coverage/BUILD.bazel @@ -0,0 +1,11 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "coverage_test", + srcs = ["coverage_test.go"], +) + +go_bazel_test( + name = "gen_code_test", + srcs = ["gen_code_test.go"], +) diff --git a/tests/core/nogo/coverage/README.rst b/tests/core/nogo/coverage/README.rst new file mode 100644 index 00000000..2bb6de95 --- /dev/null +++ b/tests/core/nogo/coverage/README.rst @@ -0,0 +1,23 @@ +nogo test with coverage +======================= + +.. _nogo: /go/nogo.rst +.. _#1940: https://github.com/bazelbuild/rules_go/issues/1940 +.. _#2146: https://github.com/bazelbuild/rules_go/issues/2146 + +Tests to ensure that `nogo`_ works with coverage. + +coverage_test +------------- +Checks that `nogo`_ works when coverage is enabled. All covered libraries gain +an implicit dependencies on ``//go/tools/coverdata``, which is a +`go_tool_library`_, which isn't built with `nogo`_. We should be able to +handle libraries like this that do not have serialized facts. Verifies `#1940`_. + +Also checks that `nogo`_ itself can be built with coverage enabled. +Verifies `#2146`_. + +gen_code_test +------------- +Checks how `nogo`_ should not run on source code that was generated as part of +rules_go's coverage implementation. diff --git a/tests/core/nogo/coverage/coverage_test.go b/tests/core/nogo/coverage/coverage_test.go new file mode 100644 index 00000000..97dc63dc --- /dev/null +++ b/tests/core/nogo/coverage/coverage_test.go @@ -0,0 +1,65 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package coverage_test + +import ( + "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("@io_bazel_rules_go//go:def.bzl", "go_test", "go_tool_library", "nogo") + +go_test( + name = "coverage_target", + srcs = ["coverage_target_test.go"], + deps = [":coverage_target_dep"], +) + +go_tool_library( + name = "coverage_target_dep", + importmap = "mapped/coverage_target/dep", + importpath = "coverage_target/dep", +) + +nogo( + name = "nogo", + vet = True, + visibility = ["//visibility:public"], +) +-- coverage_target_test.go -- +package coverage_target_test +`, + Nogo: `@//:nogo`, + }) +} + +func TestCoverageWithNogo(t *testing.T) { + if out, err := bazel_testing.BazelOutput("coverage", "//:coverage_target"); err != nil { + println(string(out)) + t.Fatal(err) + } +} + +func TestCoverageOfNogo(t *testing.T) { + if out, err := bazel_testing.BazelOutput("build", "--instrumentation_filter=.*", "--collect_code_coverage", "//:nogo"); err != nil { + println(string(out)) + t.Fatal(err) + } +} diff --git a/tests/core/nogo/coverage/gen_code_test.go b/tests/core/nogo/coverage/gen_code_test.go new file mode 100644 index 00000000..6f2c55b6 --- /dev/null +++ b/tests/core/nogo/coverage/gen_code_test.go @@ -0,0 +1,130 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gen_code_test + +import ( + "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("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test", "nogo") + +go_library( + name = "simple", + srcs = ["simple.go"], + importpath = "simple" +) + +go_test( + name = "simple_test", + srcs = ["simple_test.go"], + embed = [":simple"] +) + +nogo( + name = "nogo", + deps = ["//nocover"], + visibility = ["//visibility:public"], +) +-- simple.go -- +package simple + +func Foo() string { + return "foo" +} +-- simple_test.go -- +package simple + +import "testing" + +func TestFoo(t *testing.T) { + if actual, expected := Foo(), "foo"; actual != expected { + t.Errorf("Foo() should return foo") + } +} +-- nocover/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "nocover", + srcs = ["analyzer.go"], + importpath = "nocover", + visibility = ["//visibility:public"], + deps = [ + "@org_golang_x_tools//go/analysis", + "@org_golang_x_tools//go/analysis/passes/inspect", + "@org_golang_x_tools//go/ast/inspector", + ], +) +-- nocover/analyzer.go -- +package nocover + +import ( + "fmt" + "go/ast" + "strings" + + "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: "nocover", + Doc: "nocover ensure that source code was not a generated file created by rules_go's coverage implementation", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + inspector.Preorder([]ast.Node{(*ast.ValueSpec)(nil)}, func(node ast.Node) { + valueSpec := node.(*ast.ValueSpec) + if len(valueSpec.Names) != 1 { + return + } + + varName := valueSpec.Names[0].Name + + // check for coverage variable name that matches this pattern: CoverZ%sZ%dZ%s + // see go/tools/builders/compilepkg.go -> coverVar for more information + if strings.HasPrefix(varName, "CoverZ") && strings.Count(varName, "Z") >= 3 { + pass.Report(analysis.Diagnostic{ + Pos: valueSpec.Pos(), + End: valueSpec.End(), + Message: fmt.Sprintf("variable %s was generated by rules_go", varName), + }) + } + }) + + return nil, nil +} +`, + Nogo: `@//:nogo`, + }) +} + +func TestNogoCoverGenCode(t *testing.T) { + if out, err := bazel_testing.BazelOutput("coverage", "//:simple_test"); err != nil { + println(string(out)) + t.Fatal(err) + } +} diff --git a/tests/core/nogo/custom/BUILD.bazel b/tests/core/nogo/custom/BUILD.bazel new file mode 100644 index 00000000..06317a0b --- /dev/null +++ b/tests/core/nogo/custom/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "custom_test", + srcs = ["custom_test.go"], +) diff --git a/tests/core/nogo/custom/README.rst b/tests/core/nogo/custom/README.rst new file mode 100644 index 00000000..285c8ff6 --- /dev/null +++ b/tests/core/nogo/custom/README.rst @@ -0,0 +1,16 @@ +Custom nogo analyzers +===================== + +.. _nogo: /go/nogo.rst +.. _go_library: /docs/go/core/rules.md#_go_library + +Tests to ensure that custom `nogo`_ analyzers run and detect errors. + +.. contents:: + +custom_test +----------- +Verifies that custom analyzers print errors and fail a `go_library`_ build when +a configuration file is not provided, and that analyzers with the same package +name do not conflict. Also checks that custom analyzers can be configured to +apply only to certain file paths using a custom configuration file. diff --git a/tests/core/nogo/custom/custom_test.go b/tests/core/nogo/custom/custom_test.go new file mode 100644 index 00000000..27624431 --- /dev/null +++ b/tests/core/nogo/custom/custom_test.go @@ -0,0 +1,510 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +const origConfig = `# config = "",` + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Nogo: "@//:nogo", + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") + +nogo( + name = "nogo", + deps = [ + ":foofuncname", + ":importfmt", + ":visibility", + ], + # config = "", + visibility = ["//visibility:public"], +) + +go_library( + name = "importfmt", + srcs = ["importfmt.go"], + importpath = "importfmtanalyzer", + deps = ["@org_golang_x_tools//go/analysis"], + visibility = ["//visibility:public"], +) + +go_library( + name = "foofuncname", + srcs = ["foofuncname.go"], + importpath = "foofuncanalyzer", + deps = ["@org_golang_x_tools//go/analysis"], + visibility = ["//visibility:public"], +) + +go_library( + name = "visibility", + srcs = ["visibility.go"], + importpath = "visibilityanalyzer", + deps = [ + "@org_golang_x_tools//go/analysis", + "@org_golang_x_tools//go/ast/inspector", + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "has_errors", + srcs = ["has_errors.go"], + importpath = "haserrors", + deps = [":dep"], +) + +go_library( + name = "has_errors_linedirective", + srcs = ["has_errors_linedirective.go"], + importpath = "haserrors_linedirective", + deps = [":dep"], +) + +go_library( + name = "uses_cgo_with_errors", + srcs = [ + "examplepkg/uses_cgo_clean.go", + "examplepkg/pure_src_with_err_calling_native.go", + ], + importpath = "examplepkg", + cgo = True, +) + +go_library( + name = "no_errors", + srcs = ["no_errors.go"], + importpath = "noerrors", + deps = [":dep"], +) + +go_library( + name = "dep", + srcs = ["dep.go"], + importpath = "dep", +) + +-- foofuncname.go -- +// importfmt checks for functions named "Foo". +// It has the same package name as another check to test the checks with +// the same package name do not conflict. +package importfmt + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" +) + +const doc = "report calls of functions named \"Foo\"\n\nThe foofuncname analyzer reports calls to functions that are\nnamed \"Foo\"." + +var Analyzer = &analysis.Analyzer{ + Name: "foofuncname", + Run: run, + Doc: doc, +} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, f := range pass.Files { + // TODO(samueltan): use package inspector once the latest golang.org/x/tools + // changes are pulled into this branch (see #1755). + ast.Inspect(f, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.FuncDecl: + if n.Name.Name == "Foo" { + pass.Reportf(n.Pos(), "function must not be named Foo") + } + return true + } + return true + }) + } + return nil, nil +} + +-- importfmt.go -- +// importfmt checks for the import of package fmt. +package importfmt + +import ( + "go/ast" + "strconv" + + "golang.org/x/tools/go/analysis" +) + +const doc = "report imports of package fmt\n\nThe importfmt analyzer reports imports of package fmt." + +var Analyzer = &analysis.Analyzer{ + Name: "importfmt", + Run: run, + Doc: doc, +} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, f := range pass.Files { + // TODO(samueltan): use package inspector once the latest golang.org/x/tools + // changes are pulled into this branch (see #1755). + ast.Inspect(f, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.ImportSpec: + if path, _ := strconv.Unquote(n.Path.Value); path == "fmt" { + pass.Reportf(n.Pos(), "package fmt must not be imported") + } + return true + } + return true + }) + } + return nil, nil +} + +-- visibility.go -- +// visibility looks for visibility annotations on functions and +// checks they are only called from packages allowed to call them. +package visibility + +import ( + "encoding/gob" + "go/ast" + "regexp" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" +) + +var Analyzer = &analysis.Analyzer{ + Name: "visibility", + Run: run, + Doc: "enforce visibility requirements for functions\n\nThe visibility analyzer reads visibility annotations on functions and\nchecks that packages that call those functions are allowed to do so.", + FactTypes: []analysis.Fact{(*VisibilityFact)(nil)}, +} + +type VisibilityFact struct { + Paths []string +} + +func (_ *VisibilityFact) AFact() {} // dummy method to satisfy interface + +func init() { gob.Register((*VisibilityFact)(nil)) } + +var visibilityRegexp = regexp.MustCompile("visibility:([^\\s]+)") + +func run(pass *analysis.Pass) (interface{}, error) { + in := inspector.New(pass.Files) + + // Find visibility annotations on function declarations. + in.Nodes([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node, push bool) (prune bool) { + if !push { + return false + } + + fn := n.(*ast.FuncDecl) + + if fn.Doc == nil { + return true + } + obj := pass.TypesInfo.ObjectOf(fn.Name) + if obj == nil { + return true + } + doc := fn.Doc.Text() + + if matches := visibilityRegexp.FindAllStringSubmatch(doc, -1); matches != nil { + fact := &VisibilityFact{Paths: make([]string, len(matches))} + for i, m := range matches { + fact.Paths[i] = m[1] + } + pass.ExportObjectFact(obj, fact) + } + + return true + }) + + // Find calls that may be affected by visibility declarations. + in.Nodes([]ast.Node{(*ast.CallExpr)(nil)}, func(n ast.Node, push bool) (prune bool) { + if !push { + return false + } + + callee, ok := n.(*ast.CallExpr).Fun.(*ast.SelectorExpr) + if !ok { + return false + } + obj := pass.TypesInfo.ObjectOf(callee.Sel) + if obj == nil { + return false + } + var fact VisibilityFact + if ok := pass.ImportObjectFact(obj, &fact); !ok { + return false + } + visible := false + for _, path := range fact.Paths { + if path == pass.Pkg.Path() { + visible = true + break + } + } + if !visible { + pass.Reportf(callee.Pos(), "function %s is not visible in this package", callee.Sel.Name) + } + + return false + }) + + return nil, nil +} + +-- config.json -- +{ + "importfmt": { + "only_files": { + "has_errors\\.go": "" + } + }, + "foofuncname": { + "description": "no exemptions since we know this check is 100% accurate" + }, + "visibility": { + "exclude_files": { + "has_.*\\.go": "special exception to visibility rules" + } + } +} + +-- baseconfig.json -- +{ + "_base": { + "exclude_files": { + "has_.*\\.go": "Visibility analyzer not specified. Still inherits this special exception." + } + }, + "importfmt": { + "only_files": { + "has_errors\\.go": "" + } + }, + "foofuncname": { + "description": "no exemptions since we know this check is 100% accurate, so override base config", + "exclude_files": {} + } +} + +-- has_errors.go -- +package haserrors + +import ( + _ "fmt" // This should fail importfmt + + "dep" +) + +func Foo() bool { // This should fail foofuncname + dep.D() // This should fail visibility + return true +} + +-- has_errors_linedirective.go -- +//line linedirective.go:1 +package haserrors_linedirective + +import ( + /*line linedirective_importfmt.go:4*/ _ "fmt" // This should fail importfmt + + "dep" +) + +//line linedirective_foofuncname.go:9 +func Foo() bool { // This should fail foofuncname +//line linedirective_visibility.go:10 + dep.D() // This should fail visibility + return true +} + +-- no_errors.go -- +// package noerrors contains no analyzer errors. +package noerrors + +import "dep" + +func Baz() int { + dep.D() + return 1 +} + +-- dep.go -- +package dep + +// visibility:noerrors +func D() { +} + +-- examplepkg/uses_cgo_clean.go -- +package examplepkg + +// #include <stdlib.h> +import "C" + +func Bar() bool { + if C.rand() > 10 { + return true + } + return false +} + +-- examplepkg/pure_src_with_err_calling_native.go -- +package examplepkg + +func Foo() bool { // This should fail foofuncname + return Bar() +} + +`, + }) +} + +func Test(t *testing.T) { + for _, test := range []struct { + desc, config, target string + wantSuccess bool + includes, excludes []string + }{ + { + desc: "default_config", + target: "//:has_errors", + wantSuccess: false, + includes: []string{ + `has_errors.go:.*package fmt must not be imported \(importfmt\)`, + `has_errors.go:.*function must not be named Foo \(foofuncname\)`, + `has_errors.go:.*function D is not visible in this package \(visibility\)`, + }, + }, { + desc: "default_config_linedirective", + target: "//:has_errors_linedirective", + wantSuccess: false, + includes: []string{ + `linedirective_importfmt.go:.*package fmt must not be imported \(importfmt\)`, + `linedirective_foofuncname.go:.*function must not be named Foo \(foofuncname\)`, + `linedirective_visibility.go:.*function D is not visible in this package \(visibility\)`, + }, + }, { + desc: "custom_config", + config: "config.json", + target: "//:has_errors", + wantSuccess: false, + includes: []string{ + `has_errors.go:.*package fmt must not be imported \(importfmt\)`, + `has_errors.go:.*function must not be named Foo \(foofuncname\)`, + }, + excludes: []string{ + `visib`, + }, + }, { + desc: "custom_config_linedirective", + config: "config.json", + target: "//:has_errors_linedirective", + wantSuccess: false, + includes: []string{ + `linedirective_foofuncname.go:.*function must not be named Foo \(foofuncname\)`, + `linedirective_visibility.go:.*function D is not visible in this package \(visibility\)`, + }, + excludes: []string{ + `importfmt`, + }, + }, { + desc: "custom_config_with_base_linedirective", + config: "baseconfig.json", + target: "//:has_errors_linedirective", + wantSuccess: false, + includes: []string{ + `linedirective_foofuncname.go:.*function must not be named Foo \(foofuncname\)`, + `linedirective_visibility.go:.*function D is not visible in this package \(visibility\)`, + }, + excludes: []string{ + `importfmt`, + }, + }, { + desc: "uses_cgo_with_errors", + config: "config.json", + target: "//:uses_cgo_with_errors", + wantSuccess: false, + includes: []string{ + // note the cross platform regex :) + `.*[\\/]cgo[\\/]examplepkg[\\/]pure_src_with_err_calling_native.go:.*function must not be named Foo \(foofuncname\)`, + }, + }, { + desc: "no_errors", + target: "//:no_errors", + wantSuccess: true, + excludes: []string{"no_errors.go"}, + }, + } { + t.Run(test.desc, func(t *testing.T) { + if test.config != "" { + customConfig := fmt.Sprintf("config = %q,", test.config) + if err := replaceInFile("BUILD.bazel", origConfig, customConfig); err != nil { + t.Fatal(err) + } + defer replaceInFile("BUILD.bazel", customConfig, origConfig) + } + + 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.Fatalf("unexpected error: %v", err) + } + + for _, pattern := range test.includes { + if matched, err := regexp.Match(pattern, stderr.Bytes()); err != nil { + t.Fatal(err) + } else if !matched { + t.Errorf("got output:\n %s\n which does not contain pattern: %s", string(stderr.Bytes()), 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) + } + } + }) + } +} + +func replaceInFile(path, old, new string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + data = bytes.ReplaceAll(data, []byte(old), []byte(new)) + return ioutil.WriteFile(path, data, 0666) +} diff --git a/tests/core/nogo/custom/flags/BUILD.bazel b/tests/core/nogo/custom/flags/BUILD.bazel new file mode 100644 index 00000000..cdf4c76a --- /dev/null +++ b/tests/core/nogo/custom/flags/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "flags_test", + srcs = ["flags_test.go"], +) diff --git a/tests/core/nogo/custom/flags/README.rst b/tests/core/nogo/custom/flags/README.rst new file mode 100644 index 00000000..e525250b --- /dev/null +++ b/tests/core/nogo/custom/flags/README.rst @@ -0,0 +1,19 @@ +Custom nogo analyzer flags +===================== + +.. _nogo: /go/nogo.rst +.. _go_library: /docs/go/core/rules.md#_go_library + +Tests to ensure that custom `nogo`_ analyzers that consume flags can be +supplied those flags via nono config. + +.. contents:: + +flags_test +----------- +Verifies that a simple custom analyzer's behavior can be modified by setting +its analyzer flags in the nogo driver, and that these flags can be provided to +the driver via the nogo config `analyzer_flags` field. Also checks that +invalid flags as defined by the `flag` package cause the driver to immediately +return an error. + diff --git a/tests/core/nogo/custom/flags/flags_test.go b/tests/core/nogo/custom/flags/flags_test.go new file mode 100644 index 00000000..7381a3f0 --- /dev/null +++ b/tests/core/nogo/custom/flags/flags_test.go @@ -0,0 +1,262 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flags_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +const origConfig = `# config = "",` + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Nogo: "@//:nogo", + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") + +nogo( + name = "nogo", + deps = [ + ":flagger", + ], + # config = "", + visibility = ["//visibility:public"], +) + +go_library( + name = "flagger", + srcs = ["flagger.go"], + importpath = "flaggeranalyzer", + deps = [ + "@org_golang_x_tools//go/analysis", + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "some_file", + srcs = ["some_file.go"], + importpath = "somefile", + deps = [":dep"], +) + +go_library( + name = "dep", + srcs = ["dep.go"], + importpath = "dep", +) + +-- flagger.go -- +// flagger crashes when three flags are set in the config or else it no-ops +package flagger + +import ( + "errors" + + "golang.org/x/tools/go/analysis" +) + +var ( + boolSwitch bool + stringSwitch string + intSwitch int +) + +var Analyzer = &analysis.Analyzer{ + Name: "flagger", + Run: run, + Doc: "Dummy analyzer that crashes when all its flags are set correctly", +} + +func init() { + Analyzer.Flags.BoolVar(&boolSwitch, "bool-switch", false, "Bool must be set to true to run") + Analyzer.Flags.StringVar(&stringSwitch, "string-switch", "no", "String must be set to \"yes\" to run") + Analyzer.Flags.IntVar(&intSwitch, "int-switch", 0, "Int must be set to 1 to run") +} + +func run(pass *analysis.Pass) (interface{}, error) { + if !boolSwitch { + return nil, nil + } + if stringSwitch != "yes" { + return nil, nil + } + if intSwitch != 1 { + return nil, nil + } + return nil, errors.New("all switches were set -> fail") +} + +-- all_flags_set.json -- +{ + "flagger": { + "description": "this will crash on every file", + "analyzer_flags": { + "bool-switch": "true", + "int-switch": "1", + "string-switch": "yes" + } + } +} + +-- two_flags_set.json -- +{ + "flagger": { + "description": "this will succeed on every file", + "analyzer_flags": { + "bool-switch": "true", + "int-switch": "1" + } + } +} + +-- invalid_int.json -- +{ + "flagger": { + "description": "this will crash immediately due to an invalid int flag", + "analyzer_flags": { + "int-switch": "one", + "string-switch": "yes" + } + } +} + +-- nonexistent_flag.json -- +{ + "flagger": { + "description": "this will crash immediately due to a nonexistent flag", + "analyzer_flags": { + "int-switch": "1", + "bool-switch": "true", + "string-switch": "yes", + "description": "This is a good analyzer" + } + } +} + +-- hyphenated_flag.json -- +{ + "flagger": { + "description": "this will crash immediately due to a hyphenated flag", + "analyzer_flags": { + "-int-switch": "1" + } + } +} + +-- some_file.go -- +// package somefile contains a file and has a dep +package somefile + +import "dep" + +func Baz() int { + dep.D() + return 1 +} + +-- dep.go -- +package dep + +func D() { +} + +`, + }) +} + +func Test(t *testing.T) { + for _, test := range []struct { + desc, config string + wantSuccess bool + includes, excludes []string + }{ + { + desc: "config_flags_triggering_error", + wantSuccess: false, + config: "all_flags_set.json", + includes: []string{"all switches were set -> fail"}, + }, { + desc: "config_flags_triggering_success", + wantSuccess: true, + config: "two_flags_set.json", + }, { + desc: "invalid_int_triggering_error", + wantSuccess: false, + config: "invalid_int.json", + includes: []string{"flagger: invalid value for flag: int-switch=one"}, + }, { + desc: "nonexistent_flag_triggering_error", + wantSuccess: false, + config: "nonexistent_flag.json", + includes: []string{"flagger: unrecognized flag: description"}, + }, { + desc: "hyphenated_flag_triggering_error", + wantSuccess: false, + config: "hyphenated_flag.json", + includes: []string{"flagger: flag should not begin with '-': -int-switch"}, + }, + } { + t.Run(test.desc, func(t *testing.T) { + if test.config != "" { + customConfig := fmt.Sprintf("config = %q,", test.config) + if err := replaceInFile("BUILD.bazel", origConfig, customConfig); err != nil { + t.Fatal(err) + } + defer replaceInFile("BUILD.bazel", customConfig, origConfig) + } + + cmd := bazel_testing.BazelCmd("build", "//:some_file") + 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.Fatalf("unexpected error: %v", err) + } + + for _, pattern := range test.includes { + if matched, err := regexp.Match(pattern, stderr.Bytes()); err != nil { + t.Fatal(err) + } else if !matched { + t.Errorf("got output:\n %s\n which does not contain pattern: %s", string(stderr.Bytes()), 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) + } + } + }) + } +} + +func replaceInFile(path, old, new string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + data = bytes.ReplaceAll(data, []byte(old), []byte(new)) + return ioutil.WriteFile(path, data, 0666) +} diff --git a/tests/core/nogo/deps/BUILD.bazel b/tests/core/nogo/deps/BUILD.bazel new file mode 100644 index 00000000..a1d998fd --- /dev/null +++ b/tests/core/nogo/deps/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "deps_test", + srcs = ["deps_test.go"], +) diff --git a/tests/core/nogo/deps/README.rst b/tests/core/nogo/deps/README.rst new file mode 100644 index 00000000..93affd23 --- /dev/null +++ b/tests/core/nogo/deps/README.rst @@ -0,0 +1,26 @@ +nogo analyzers with dependencies +============================= + +.. _nogo: /go/nogo.rst +.. _go_library: /docs/go/core/rules.md#_go_library + +Tests to ensure that custom `nogo`_ analyzers that depend on each other are +run in the correct order. + +.. contents:: + +deps_test +--------- +Given the following dependency graph of analyzers: + + a ----+ + | + v + b --> c --> d + +Where analyzers a, b, c are explicitly depended on by the `nogo`_ rule and d +isn't, verifies that a `go_library`_ build causes both paths in the graph +(a->c->d and b->c->d) to be executed, and that each analyzer runs exactly once. + +Also verify that the diagnostics reported by d are not printed to the build log +since d was not explicitly depended on by the declared `nogo`_ rule. diff --git a/tests/core/nogo/deps/deps_test.go b/tests/core/nogo/deps/deps_test.go new file mode 100644 index 00000000..f527effd --- /dev/null +++ b/tests/core/nogo/deps/deps_test.go @@ -0,0 +1,211 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deps_test + +import ( + "bytes" + "regexp" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Nogo: "@//:nogo", + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") + +nogo( + name = "nogo", + deps = [ + ":a", + ":b", + ":c", + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "a", + srcs = ["a.go"], + importpath = "a", + deps = [ + ":c", + "@org_golang_x_tools//go/analysis" + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "b", + srcs = ["b.go"], + importpath = "b", + deps = [ + ":c", + "@org_golang_x_tools//go/analysis" + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "c", + srcs = ["c.go"], + importpath = "c", + deps = [ + ":d", + "@org_golang_x_tools//go/analysis" + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "d", + srcs = ["d.go"], + importpath = "d", + deps = ["@org_golang_x_tools//go/analysis"], + visibility = ["//visibility:public"], +) + +go_library( + name = "src", + srcs = ["src.go"], + importpath = "src", +) + +-- a.go -- +package a + +import ( + "c" + "go/token" + + "golang.org/x/tools/go/analysis" +) + +var Analyzer = &analysis.Analyzer{ + Name: "a", + Doc: "an analyzer that depends on c.Analyzer", + Run: run, + Requires: []*analysis.Analyzer{c.Analyzer}, +} + +func run(pass *analysis.Pass) (interface{}, error) { + pass.Reportf(token.NoPos, "a %s", pass.ResultOf[c.Analyzer]) + return nil, nil +} + +-- b.go -- +package b + +import ( + "c" + "go/token" + + "golang.org/x/tools/go/analysis" +) + +var Analyzer = &analysis.Analyzer{ + Name: "b", + Doc: "an analyzer that depends on c.Analyzer", + Run: run, + Requires: []*analysis.Analyzer{c.Analyzer}, +} + +func run(pass *analysis.Pass) (interface{}, error) { + pass.Reportf(token.NoPos, "b %s", pass.ResultOf[c.Analyzer]) + return nil, nil +} + +-- c.go -- +package c + +import ( + "d" + "fmt" + "go/token" + "reflect" + + "golang.org/x/tools/go/analysis" +) + +var Analyzer = &analysis.Analyzer{ + Name: "c", + Doc: "an analyzer that depends on d.Analyzer", + Run: run, + Requires: []*analysis.Analyzer{d.Analyzer}, + ResultType: reflect.TypeOf(""), +} + +func run(pass *analysis.Pass) (interface{}, error) { + pass.Reportf(token.NoPos, "only printed once") + return fmt.Sprintf("c %s", pass.ResultOf[d.Analyzer]), nil +} + +-- d.go -- +package d + +import ( + "go/token" + "reflect" + + "golang.org/x/tools/go/analysis" +) + +var Analyzer = &analysis.Analyzer{ + Name: "d", + Doc: "an analyzer that does not depend on other analyzers", + Run: run, + ResultType: reflect.TypeOf(""), +} + +func run(pass *analysis.Pass) (interface{}, error) { + pass.Reportf(token.NoPos, "this should not be printed") + return "d", nil +} + +-- src.go -- +package src + +func Foo() int { + return 1 +} + +`, + }) +} + +func Test(t *testing.T) { + cmd := bazel_testing.BazelCmd("build", "//:src") + stderr := &bytes.Buffer{} + cmd.Stderr = stderr + if err := cmd.Run(); err == nil { + t.Fatal("unexpected success") + } + + for _, pattern := range []string{ + "a c d", + "b c d", + "only printed once", + } { + if matched, _ := regexp.Match(pattern, stderr.Bytes()); !matched { + t.Errorf("output does not contain pattern: %s", pattern) + } + } + if bytes.Contains(stderr.Bytes(), []byte("this should not be printed")) { + t.Errorf("%q was printed", "this should not be printed") + } +} diff --git a/tests/core/nogo/generate/BUILD.bazel b/tests/core/nogo/generate/BUILD.bazel new file mode 100644 index 00000000..52567b61 --- /dev/null +++ b/tests/core/nogo/generate/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "empty_test", + srcs = ["empty_test.go"], +) diff --git a/tests/core/nogo/generate/README.rst b/tests/core/nogo/generate/README.rst new file mode 100644 index 00000000..d9604fab --- /dev/null +++ b/tests/core/nogo/generate/README.rst @@ -0,0 +1,12 @@ +nogo test with generated code +======================= + +.. _nogo: /go/nogo.rst + +Tests to ensure `nogo`_ interaction with generated code. + +empty_test +------------- +Checks that `nogo`_ is not running over the `_empty.go` file that was +generated as part of GoCompilePkg. + diff --git a/tests/core/nogo/generate/empty_test.go b/tests/core/nogo/generate/empty_test.go new file mode 100644 index 00000000..a4f8de12 --- /dev/null +++ b/tests/core/nogo/generate/empty_test.go @@ -0,0 +1,102 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package empty_test + +import ( + "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("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test", "nogo") + +go_test( + name = "simple_test", + size = "small", + srcs = ["simple_test.go"], +) + +nogo( + name = "nogo", + deps = ["//noempty"], + visibility = ["//visibility:public"], +) +-- simple_test.go -- +package simple + +import ( + "testing" +) + +func TestFoo(t *testing.T) { +} +-- noempty/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "noempty", + srcs = ["analyzer.go"], + importpath = "noempty", + visibility = ["//visibility:public"], + deps = [ + "@org_golang_x_tools//go/analysis", + ], +) +-- noempty/analyzer.go -- +package noempty + +import ( + "fmt" + "path/filepath" + "strings" + + "golang.org/x/tools/go/analysis" +) + +var Analyzer = &analysis.Analyzer{ + Name: "noempty", + Doc: "noempty ensure that source code was not a generated file created by rules_go test rewrite", + Run: run, +} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, f := range pass.Files { + pos := pass.Fset.PositionFor(f.Pos(), false) + + if strings.HasSuffix(pos.Filename, filepath.Join(".", "_empty.go")) { + pass.Report(analysis.Diagnostic{ + Pos: 0, + Message: fmt.Sprintf("Detected generated source code from rules_go: %s", pos.Filename), + }) + } + } + + return nil, nil +} +`, + Nogo: `@//:nogo`, + }) +} + +func TestNogoGenEmptyCode(t *testing.T) { + if out, err := bazel_testing.BazelOutput("build", "-k", "//:simple_test"); err != nil { + println(string(out)) + t.Fatal(err) + } +} diff --git a/tests/core/nogo/generics/BUILD.bazel b/tests/core/nogo/generics/BUILD.bazel new file mode 100644 index 00000000..a40df345 --- /dev/null +++ b/tests/core/nogo/generics/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "generics_test", + srcs = ["generics_test.go"], +) diff --git a/tests/core/nogo/generics/README.rst b/tests/core/nogo/generics/README.rst new file mode 100644 index 00000000..c348a992 --- /dev/null +++ b/tests/core/nogo/generics/README.rst @@ -0,0 +1,18 @@ +nogo analyzers run against code using generics +============================================== + +.. _nogo: /go/nogo.rst +.. _buildssa: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/buildssa +.. _nilness: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilness + +Tests to ensure that `nogo`_ analyzers that run on code using generics get correct +type instantiation information. + +.. contents:: + +generics_test +------------- + +Verifies that code using generic types gets loaded including all type instantiation +information, so that analyzers based on the `buildssa`_ analyzer (such as `nilness`_) get +a complete picture of all types in the code. diff --git a/tests/core/nogo/generics/generics_test.go b/tests/core/nogo/generics/generics_test.go new file mode 100644 index 00000000..562d52ed --- /dev/null +++ b/tests/core/nogo/generics/generics_test.go @@ -0,0 +1,85 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generics_test + +import ( + "bytes" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Nogo: "@//:nogo", + Main: ` +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo") + +nogo( + name = "nogo", + visibility = ["//visibility:public"], + deps = ["@org_golang_x_tools//go/analysis/passes/buildssa"], +) + +go_library( + name = "src", + srcs = ["src.go"], + importpath = "src", +) + +-- src.go -- +package src + +type Set[T comparable] struct { + m map[T]struct{} +} + +func New[T comparable](s ...T) *Set[T] { + set := &Set[T]{} + set.Add(s...) + return set +} + +func (set *Set[T]) Add(s ...T) { + if set.m == nil { + set.m = make(map[T]struct{}) + } + for _, s := range s { + set.m[s] = struct{}{} + } +} + +func S(x ...string) *Set[string] { + return New[string](x...) +} +`, + }) +} + +func Test(t *testing.T) { + cmd := bazel_testing.BazelCmd("build", "//:src") + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + t.Log("output:", stderr.String()) + t.Fatal("unexpected error:", err) + } + + if bytes.Contains(stderr.Bytes(), []byte("panic")) { + t.Errorf("found panic in Bazel output: \n%s", stderr.String()) + } +} diff --git a/tests/core/nogo/nolint/BUILD.bazel b/tests/core/nogo/nolint/BUILD.bazel new file mode 100644 index 00000000..c828045c --- /dev/null +++ b/tests/core/nogo/nolint/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "nolint_test", + srcs = ["nolint_test.go"], +) diff --git a/tests/core/nogo/nolint/README.rst b/tests/core/nogo/nolint/README.rst new file mode 100644 index 00000000..83436898 --- /dev/null +++ b/tests/core/nogo/nolint/README.rst @@ -0,0 +1,14 @@ +Nolint check +========= + +.. _go_library: /docs/go/core/rules.md#_go_library + +Tests to ensure that errors found by nogo and annotated with //nolint are +ignored. + +.. contents:: + +nolint_test +-------- +Verified that errors emitted by ``nogo`` are ignored when `//nolint` appears as +a comment. diff --git a/tests/core/nogo/nolint/nolint_test.go b/tests/core/nogo/nolint/nolint_test.go new file mode 100644 index 00000000..78ecbbcf --- /dev/null +++ b/tests/core/nogo/nolint/nolint_test.go @@ -0,0 +1,227 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nolint_test + +import ( + "bytes" + "io/ioutil" + "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("@io_bazel_rules_go//go:def.bzl", "go_library", "go_tool_library", "nogo") + +nogo( + name = "nogo", + vet = True, + deps = ["@org_golang_x_tools//go/analysis/passes/nilness"], + visibility = ["//visibility:public"], +) + +go_library( + name = "inline", + srcs = ["inline.go"], + importpath = "test", +) + +go_library( + name = "inline_filter", + srcs = ["inline_filter.go"], + importpath = "test", +) + +go_library( + name = "block", + srcs = ["block.go"], + importpath = "test", +) + +go_library( + name = "block_multiline", + srcs = ["block_multiline.go"], + importpath = "test", +) + +go_library( + name = "inline_errors", + srcs = ["inline_errors.go"], + importpath = "test", +) + +go_library( + name = "inline_column", + srcs = ["inline_column.go"], + importpath = "test", +) + +go_library( + name = "large_block", + srcs = ["large_block.go"], + importpath = "test", +) +-- inline.go -- +package test + +import "fmt" + +func F() { + s := "hello" + fmt.Printf("%d", s) //nolint +} + +-- inline_filter.go -- +package test + +func F() bool { + return true || true //nolint:bools +} + +-- block.go -- +package test + +import "fmt" + +func F() { + //nolint + fmt.Printf("%d", "hello") +} + +-- block_multiline.go -- +package test + +func F() bool { + var i *int + //nolint + return true && + i != nil +} + +-- inline_errors.go -- +package test + +import "fmt" + +func F() { + var i *int + if i == nil { + fmt.Printf("%d", "hello") //nolint + fmt.Println(*i) // Keep nil deref error + } +} + +-- inline_column.go -- +package test + +import "fmt" + +func F() { + // Purposely used 'helo' to align the column + fmt.Printf("%d", "helo") //nolint + superLongVariableName := true || true + var _ = superLongVariableName +} + +-- large_block.go -- +package test + +import "fmt" + +var V = struct { + S string + B bool +} { + S: fmt.Sprintf("%d", "hello"), //nolint + B: true || true, +} +`, + }) +} + +func Test(t *testing.T) { + customRegister := `go_register_toolchains(nogo = "@//:nogo")` + if err := replaceInFile("WORKSPACE", "go_register_toolchains()", customRegister); err != nil { + t.Fatal(err) + } + + tests := []struct { + Name string + Target string + Expected string + }{ + { + Name: "Inline comment", + Target: "//:inline", + }, + { + Name: "Inline with lint filter", + Target: "//:inline_filter", + }, + { + Name: "Block comment", + Target: "//:block", + }, + { + Name: "Multiline block comment", + Target: "//:block_multiline", + }, + { + Name: "Inline with errors", + Target: "//:inline_errors", + Expected: "inline_errors.go:9:15: nil dereference in load (nilness)", + }, + { + Name: "Inline comment on same column does not apply", + Target: "//:inline_column", + Expected: "inline_column.go:8:27: redundant or: true || true (bools)", + }, + { + Name: "Inline comment does not apply to larger block", + Target: "//:large_block", + Expected: "large_block.go:10:5: redundant or: true || true (bools)", + }, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + cmd := bazel_testing.BazelCmd("build", tc.Target) + b, err := cmd.CombinedOutput() + output := string(b) + if tc.Expected != "" && err == nil { + t.Fatal("unexpected success", output) + } + if tc.Expected == "" && err != nil { + t.Fatal("unexpected failure", output) + } + if !strings.Contains(output, tc.Expected) { + t.Errorf("output did not contain expected: %s\n%s", tc.Expected, output) + } + }) + } +} + +func replaceInFile(path, old, new string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + data = bytes.ReplaceAll(data, []byte(old), []byte(new)) + return ioutil.WriteFile(path, data, 0666) +} diff --git a/tests/core/nogo/vet/BUILD.bazel b/tests/core/nogo/vet/BUILD.bazel new file mode 100644 index 00000000..b01e0482 --- /dev/null +++ b/tests/core/nogo/vet/BUILD.bazel @@ -0,0 +1,6 @@ +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") + +go_bazel_test( + name = "vet_test", + srcs = ["vet_test.go"], +) diff --git a/tests/core/nogo/vet/README.rst b/tests/core/nogo/vet/README.rst new file mode 100644 index 00000000..28f95c0e --- /dev/null +++ b/tests/core/nogo/vet/README.rst @@ -0,0 +1,14 @@ +Vet check +========= + +.. _go_library: /docs/go/core/rules.md#_go_library + +Tests to ensure that vet runs and detects errors. + +.. contents:: + +vet_test +-------- +Verifies that vet errors are emitted on a `go_library`_ with problems when built +with a ``nogo`` binary with ``vet = True``. No errors should be emitted when +analyzing error-free source code. Vet should not be enabled by default. diff --git a/tests/core/nogo/vet/vet_test.go b/tests/core/nogo/vet/vet_test.go new file mode 100644 index 00000000..4bc2163f --- /dev/null +++ b/tests/core/nogo/vet/vet_test.go @@ -0,0 +1,179 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vet_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "regexp" + "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("@io_bazel_rules_go//go:def.bzl", "go_library", "go_tool_library", "nogo") + +nogo( + name = "nogo", + vet = True, + visibility = ["//visibility:public"], +) + +go_library( + name = "has_errors", + srcs = ["has_errors.go"], + importpath = "haserrors", + deps = [":fmtwrap"], +) + +go_library( + name = "no_errors", + srcs = ["no_errors.go"], + cgo = True, + importpath = "noerrors", +) + +go_library( + name = "fmtwrap", + srcs = ["fmtwrap.go"], + importpath = "fmtwrap", +) + +-- has_errors.go -- +package haserrors + +// +build build_tags_error + +import ( + "fmtwrap" + "sync/atomic" +) + +func F() {} + +func Foo() bool { + x := uint64(1) + _ = atomic.AddUint64(&x, 1) + if F == nil { // nilfunc error. + return false + } + fmtwrap.Printf("%b", "hi") // printf error. + return true || true // redundant boolean error. +} + +-- no_errors.go -- +package noerrors + +// const int x = 1; +import "C" + +func Foo() bool { + return bool(C.x == 1) +} + +-- fmtwrap.go -- +package fmtwrap + +import "fmt" + +func Printf(format string, args ...interface{}) { + fmt.Printf(format, args...) +} +`, + }) +} + +func Test(t *testing.T) { + for _, test := range []struct { + desc, nogo, target string + wantSuccess bool + includes, excludes []string + }{ + { + desc: "default", + target: "//:has_errors", + wantSuccess: true, + excludes: []string{ + "\\+build comment must appear before package clause and be followed by a blank line", + "comparison of function F == nil is always false", + "Printf format %b has arg \"hi\" of wrong type string", + "redundant or: true \\|\\| true", + }, + }, { + desc: "enabled_no_errors", + target: "//:no_errors", + wantSuccess: true, + }, { + desc: "enabled_has_errors", + nogo: "@//:nogo", + target: "//:has_errors", + includes: []string{ + "misplaced \\+build comment", + "comparison of function F == nil is always false", + "Printf format %b has arg \"hi\" of wrong type string", + "redundant or: true \\|\\| true", + }, + }, + } { + t.Run(test.desc, func(t *testing.T) { + if test.nogo != "" { + origRegister := "go_register_toolchains()" + customRegister := fmt.Sprintf("go_register_toolchains(nogo = %q)", test.nogo) + if err := replaceInFile("WORKSPACE", origRegister, customRegister); err != nil { + t.Fatal(err) + } + defer replaceInFile("WORKSPACE", customRegister, origRegister) + } + + 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.Fatalf("unexpected error: %v", err) + } + + 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", 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) + } + } + }) + } +} + +func replaceInFile(path, old, new string) error { + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + data = bytes.ReplaceAll(data, []byte(old), []byte(new)) + return ioutil.WriteFile(path, data, 0666) +} |