go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/static_test.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package appengine 16 17 import ( 18 "fmt" 19 "go/ast" 20 "go/types" 21 "os" 22 "os/exec" 23 "path" 24 "runtime" 25 "strings" 26 "testing" 27 28 "golang.org/x/tools/go/analysis" 29 "golang.org/x/tools/go/analysis/singlechecker" 30 ) 31 32 var statusAssign = &analysis.Analyzer{ 33 Name: "statusAssignment", 34 Doc: "reports direct assignments to Build.Status", 35 Run: func(pass *analysis.Pass) (any, error) { 36 for _, file := range pass.Files { 37 if strings.HasSuffix(pass.Fset.File(file.Pos()).Name(), "_test.go") { 38 continue 39 } 40 41 ast.Inspect(file, func(n ast.Node) bool { 42 if assn, ok := n.(*ast.AssignStmt); ok { 43 checkAssignStmt(pass, assn) 44 } 45 return true 46 }) 47 } 48 49 return nil, nil 50 }, 51 } 52 53 const envKey = "BUILDBUCKET_RUN_STATUS_ASSIGN_CHECKER" 54 55 func init() { 56 if os.Getenv(envKey) != "" { 57 singlechecker.Main(statusAssign) 58 } 59 } 60 61 func checkAssignStmt(pass *analysis.Pass, stmt *ast.AssignStmt) { 62 for _, exp := range stmt.Lhs { 63 ast.Inspect(exp, func(n ast.Node) bool { 64 if se, ok := n.(*ast.SelectorExpr); ok { 65 ot := pass.TypesInfo.TypeOf(se.X) 66 if ot == nil { 67 return true 68 } 69 pt, ok := ot.(*types.Pointer) 70 if !ok { 71 return true 72 } 73 nt, ok := pt.Elem().(*types.Named) 74 if !ok { 75 return true 76 } 77 typ := nt.Obj() 78 79 if typ.Pkg().Path() != "go.chromium.org/luci/buildbucket/proto" || typ.Name() != "Build" { 80 return true 81 } 82 // At this point we know the left part of the selector is a *Build 83 84 switch se.Sel.Name { 85 case "Status", "StartTime", "EndTime": 86 pass.Report(analysis.Diagnostic{ 87 Pos: stmt.Pos(), 88 Message: fmt.Sprintf("Bare assignment to Build.%s; Use protoutil.SetStatus instead.", se.Sel.Name), 89 }) 90 } 91 return false 92 } 93 return true 94 }) 95 } 96 } 97 98 func TestBuildStatusAssignment(t *testing.T) { 99 executable, err := os.Executable() 100 if err != nil { 101 t.Fatal(err) 102 } 103 104 _, curFile, _, _ := runtime.Caller(0) 105 106 cmd := exec.Command(executable, "-c", "0", path.Dir(curFile)+"/...") 107 cmd.Env = os.Environ() 108 cmd.Env = append(cmd.Env, envKey+"=1") 109 cmd.Stdout = os.Stdout 110 cmd.Stderr = os.Stderr 111 112 switch err := cmd.Run().(type) { 113 case nil: 114 case *exec.ExitError: 115 t.Fail() 116 117 default: 118 t.Fatal(err) 119 } 120 }