github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/check_lacros.go (about) 1 // Copyright 2022 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package check 6 7 import ( 8 "go/ast" 9 "go/token" 10 11 "golang.org/x/tools/go/ast/astutil" 12 13 "go.chromium.org/tast/core/cmd/tast-lint/internal/git" 14 ) 15 16 const ( 17 noLacrosStatusMsg = `Test LacrosStatus field should exist` 18 nonLiteralLacrosMetadataMsg = `Test LacrosStatus should be a single LacrosStatus value, e.g. testing.LacrosVariantNeeded` 19 noLacrosVariantUnknownMsg = `Test LacrosStatus should not be LacrosVariantUnknown. Please work out if your test needs lacros variants. To do this, see go/lacros-tast-porting or contact edcourtney@, hidehiko@, or lacros-tast@` 20 addedUnknownMetadataMsg = `tast-lint has added LacrosVariantUnknown as a placeholder but please work out if your test needs lacros variants. To do this, see go/lacros-tast-porting or contact edcourtney@, hidehiko@, or lacros-tast@` 21 noLacrosSoftwareDepsMsg = `Test SoftwareDeps dep:lacros should be added along with dep:lacros_{un}stable` 22 ) 23 24 func hasSoftwareDeps(expr ast.Expr, dep string) bool { 25 switch v := expr.(type) { 26 case *ast.BasicLit: 27 return v.Value == "\""+dep+"\"" 28 case *ast.Ident: 29 case *ast.SelectorExpr: 30 // We can't really compute the value of constants so just assume it 31 // was the dep value we are looking for. 32 return true 33 case *ast.CompositeLit: 34 for _, arg := range v.Elts { 35 if hasSoftwareDeps(arg, dep) { 36 return true 37 } 38 } 39 return false 40 case *ast.CallExpr: 41 fun, ok := v.Fun.(*ast.Ident) 42 if !ok || fun.Name != "append" { 43 return false 44 } 45 for i, arg := range v.Args { 46 isVarList := i == 0 || (i == len(v.Args)-1 && v.Ellipsis != token.NoPos) 47 if isVarList { 48 if hasSoftwareDeps(arg, dep) { 49 return true 50 } 51 } 52 if !isVarList && !hasSoftwareDeps(arg, dep) { 53 return false 54 } 55 } 56 return false 57 } 58 59 // If we can't show there is no software dep, assume it exists. 60 return true 61 } 62 63 func checkAllSoftwareDeps(fields entityFields, dep string) bool { 64 // Check for chrome SoftwareDeps. 65 s, ok := fields["SoftwareDeps"] 66 if ok && hasSoftwareDeps(s.Value, dep) { 67 return true 68 } 69 70 // Check ExtraSoftwareDeps: 71 s, ok = fields["Params"] 72 if !ok { 73 return false // Was not in SoftwareDeps and no Params. 74 } 75 76 v, ok := s.Value.(*ast.CompositeLit) 77 if !ok { 78 return true // Expect Params to be a CompositeLit. 79 } 80 81 for _, arg := range v.Elts { 82 // Extract each Param 83 v, ok := arg.(*ast.CompositeLit) 84 if !ok { 85 return false 86 } 87 88 for _, paramField := range v.Elts { 89 kv, ok := paramField.(*ast.KeyValueExpr) 90 if !ok { 91 return false 92 } 93 id, ok := kv.Key.(*ast.Ident) 94 if !ok { 95 return false 96 } 97 98 if id.Name == "ExtraSoftwareDeps" && hasSoftwareDeps(kv.Value, dep) { 99 return true 100 } 101 } 102 } 103 104 return false 105 } 106 107 func maybeRewrite(fs *token.FileSet, fields entityFields, call *ast.CallExpr, fix bool, issues []*Issue) []*Issue { 108 if fix && len(issues) > 0 { 109 f := &ast.KeyValueExpr{ 110 Key: &ast.Ident{ 111 Name: "LacrosStatus", 112 }, 113 Value: &ast.SelectorExpr{ 114 X: &ast.Ident{ 115 Name: "testing", 116 }, 117 Sel: &ast.Ident{ 118 Name: "LacrosVariantUnknown", 119 }, 120 }, 121 } 122 if kv, ok := fields["LacrosStatus"]; ok { 123 // Try rewriting the field if it already exists. 124 astutil.Apply(kv, func(c *astutil.Cursor) bool { 125 if p, ok := c.Parent().(*ast.KeyValueExpr); ok && c.Node() == p.Value { 126 c.Replace(f.Value) 127 } 128 return true 129 }, nil) 130 } else { 131 // Otherwise add it after Func which should exist. 132 // TODO: This won't add newlines, and there doesn't appear to be any 133 // way to do this using astutil. 134 astutil.Apply(call, func(c *astutil.Cursor) bool { 135 _, parentIsComposite := c.Parent().(*ast.CompositeLit) 136 kv, currentIsKeyValue := c.Node().(*ast.KeyValueExpr) 137 if parentIsComposite && currentIsKeyValue { 138 if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Func" { 139 c.InsertAfter(f) 140 return false // Only add the field once. 141 } 142 } 143 return !currentIsKeyValue // Don't recurse into keyvalues, just look at top level testing.Test fields. 144 }, nil) 145 } 146 147 return []*Issue{{ 148 Pos: fs.Position(call.Args[0].Pos()), 149 Msg: addedUnknownMetadataMsg, 150 Link: testRegistrationURL, 151 }} 152 } 153 154 return issues 155 } 156 157 // verifyLacrosStatus verifies that the LacrosStatus field is set, and that 158 // new instances of LacrosVariantUnknown are not added. 159 func verifyLacrosStatus(fs *token.FileSet, fields entityFields, path git.CommitFile, call *ast.CallExpr, fix bool) []*Issue { 160 // Check for chrome SoftwareDeps. 161 if !checkAllSoftwareDeps(fields, "chrome") { 162 return nil 163 } 164 165 // For example, extract 'LacrosStatus: testing.LacrosVariantNeeded'. 166 kv, ok := fields["LacrosStatus"] 167 if !ok { 168 return maybeRewrite(fs, fields, call, fix, []*Issue{{ 169 Pos: fs.Position(call.Args[0].Pos()), 170 Msg: noLacrosStatusMsg, 171 Link: testRegistrationURL, 172 Fixable: true, // We can only add the field, not decide what the value should be. 173 }}) 174 } 175 176 // For example, extract 'testing.LacrosVariantNeeded'. 177 sel, ok := kv.Value.(*ast.SelectorExpr) 178 if !ok { 179 return maybeRewrite(fs, fields, call, fix, []*Issue{{ 180 Pos: fs.Position(kv.Value.Pos()), 181 Msg: nonLiteralLacrosMetadataMsg, 182 Link: testRegistrationURL, 183 }}) 184 } 185 186 // For example, extract 'LacrosVariantUnknown' from 'testing.LacrosVariantNeeded'. 187 // Only disallow LacrosVariantUnknown for new tests, to avoid annoying people 188 // trying to make incidental changes to existing tests. 189 if sel.Sel.Name == "LacrosVariantUnknown" && path.Status == git.Added { 190 return maybeRewrite(fs, fields, call, fix, []*Issue{{ 191 Pos: fs.Position(kv.Value.Pos()), 192 Msg: noLacrosVariantUnknownMsg, 193 Link: testRegistrationURL, 194 }}) 195 } 196 197 return nil 198 } 199 200 // verifyLacrosSoftwareDeps verifies that the SoftwareDeps 'lacros_{un}stable' should always be 201 // defined along with 'lacros'. 202 // Make sure that 'lacros' is a superset of 'lacros_{un}stable' that represent the breakdown 203 // {un}stable dependencies. 204 // It makes it easy to share Lacros tests with the 'dep:lacros' between Chromium and ChromiumOS. 205 func verifyLacrosSoftwareDeps(fs *token.FileSet, fields entityFields, call *ast.CallExpr) []*Issue { 206 // Raise an issue only when 'lacros_{un}stable' is set without 'lacros'. 207 if (checkAllSoftwareDeps(fields, "lacros_stable") || checkAllSoftwareDeps(fields, "lacros_unstable")) && 208 !checkAllSoftwareDeps(fields, "lacros") { 209 210 pos := call.Args[0].Pos() 211 if s, ok := fields["SoftwareDeps"]; ok { 212 pos = s.Pos() 213 } 214 215 return []*Issue{{ 216 Pos: fs.Position(pos), 217 Msg: noLacrosSoftwareDepsMsg, 218 }} 219 } 220 221 return nil 222 }