github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/build_constraints.go (about) 1 /* Copyright 2022 The Bazel Authors. All rights reserved. 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 16 package golang 17 18 import ( 19 "bufio" 20 "bytes" 21 "fmt" 22 "go/build/constraint" 23 "os" 24 "strings" 25 ) 26 27 // readTags reads and extracts build tags from the block of comments 28 // and blank lines at the start of a file which is separated from the 29 // rest of the file by a blank line. Each string in the returned slice 30 // is the trimmed text of a line after a "+build" prefix. 31 // Based on go/build.Context.shouldBuild. 32 func readTags(path string) (*buildTags, error) { 33 f, err := os.Open(path) 34 if err != nil { 35 return nil, err 36 } 37 defer f.Close() 38 39 content, err := readComments(f) 40 if err != nil { 41 return nil, err 42 } 43 44 content, goBuild, _, err := parseFileHeader(content) 45 if err != nil { 46 return nil, err 47 } 48 49 if goBuild != nil { 50 x, err := constraint.Parse(string(goBuild)) 51 if err != nil { 52 return nil, err 53 } 54 55 return newBuildTags(x) 56 } 57 58 var fullConstraint constraint.Expr 59 // Search and parse +build tags 60 scanner := bufio.NewScanner(bytes.NewReader(content)) 61 for scanner.Scan() { 62 line := strings.TrimSpace(scanner.Text()) 63 64 if !constraint.IsPlusBuild(line) { 65 continue 66 } 67 68 x, err := constraint.Parse(line) 69 if err != nil { 70 return nil, err 71 } 72 73 if fullConstraint != nil { 74 fullConstraint = &constraint.AndExpr{ 75 X: fullConstraint, 76 Y: x, 77 } 78 } else { 79 fullConstraint = x 80 } 81 } 82 83 if scanner.Err() != nil { 84 return nil, scanner.Err() 85 } 86 87 if fullConstraint == nil { 88 return nil, nil 89 } 90 91 return newBuildTags(fullConstraint) 92 } 93 94 // buildTags represents the build tags specified in a file. 95 type buildTags struct { 96 // expr represents the parsed constraint expression 97 // that can be used to evaluate a file against a set 98 // of tags. 99 expr constraint.Expr 100 // rawTags represents the concrete tags that make up expr. 101 rawTags []string 102 } 103 104 // newBuildTags will return a new buildTags structure with any 105 // ignored tags filtered out from the provided constraints. 106 func newBuildTags(x constraint.Expr) (*buildTags, error) { 107 modified, err := dropNegationForIgnoredTags(pushNot(x, false)) 108 if err != nil { 109 return nil, err 110 } 111 112 rawTags, err := collectTags(modified) 113 if err != nil { 114 return nil, err 115 } 116 117 return &buildTags{ 118 expr: modified, 119 rawTags: rawTags, 120 }, nil 121 } 122 123 func (b *buildTags) tags() []string { 124 if b == nil { 125 return nil 126 } 127 128 return b.rawTags 129 } 130 131 func (b *buildTags) eval(ok func(string) bool) bool { 132 if b == nil || b.expr == nil { 133 return true 134 } 135 136 return b.expr.Eval(ok) 137 } 138 139 func (b *buildTags) empty() bool { 140 if b == nil { 141 return true 142 } 143 144 return len(b.rawTags) == 0 145 } 146 147 // dropNegationForIgnoredTags drops negations for any concrete tags that should be ignored. 148 // This is done to ensure that when ignored tags are evaluated, they can always return true 149 // without having to worry that the result will be negated later on. Ignored tags should always 150 // evaluate to true, regardless of whether they are negated or not leaving the final evaluation 151 // to happen at compile time by the compiler. 152 func dropNegationForIgnoredTags(expr constraint.Expr) (constraint.Expr, error) { 153 if expr == nil { 154 return nil, nil 155 } 156 157 switch x := expr.(type) { 158 case *constraint.TagExpr: 159 return &constraint.TagExpr{ 160 Tag: x.Tag, 161 }, nil 162 163 case *constraint.NotExpr: 164 var toRet constraint.Expr 165 // flip nots on any ignored tags 166 if tag, ok := x.X.(*constraint.TagExpr); ok && isIgnoredTag(tag.Tag) { 167 toRet = &constraint.TagExpr{ 168 Tag: tag.Tag, 169 } 170 } else { 171 fixed, err := dropNegationForIgnoredTags(x.X) 172 if err != nil { 173 return nil, err 174 } 175 toRet = &constraint.NotExpr{X: fixed} 176 } 177 178 return toRet, nil 179 180 case *constraint.AndExpr: 181 a, err := dropNegationForIgnoredTags(x.X) 182 if err != nil { 183 return nil, err 184 } 185 186 b, err := dropNegationForIgnoredTags(x.Y) 187 if err != nil { 188 return nil, err 189 } 190 191 return &constraint.AndExpr{ 192 X: a, 193 Y: b, 194 }, nil 195 196 case *constraint.OrExpr: 197 a, err := dropNegationForIgnoredTags(x.X) 198 if err != nil { 199 return nil, err 200 } 201 202 b, err := dropNegationForIgnoredTags(x.Y) 203 if err != nil { 204 return nil, err 205 } 206 207 return &constraint.OrExpr{ 208 X: a, 209 Y: b, 210 }, nil 211 212 default: 213 return nil, fmt.Errorf("unknown constraint type: %T", x) 214 } 215 } 216 217 // filterTags will traverse the provided constraint.Expr, recursively, and call 218 // the user provided ok func on concrete constraint.TagExpr structures. If the provided 219 // func returns true, the tag in question is kept, otherwise it is filtered out. 220 func visitTags(expr constraint.Expr, visit func(string)) (err error) { 221 if expr == nil { 222 return nil 223 } 224 225 switch x := expr.(type) { 226 case *constraint.TagExpr: 227 visit(x.Tag) 228 229 case *constraint.NotExpr: 230 err = visitTags(x.X, visit) 231 232 case *constraint.AndExpr: 233 err = visitTags(x.X, visit) 234 if err == nil { 235 err = visitTags(x.Y, visit) 236 } 237 238 case *constraint.OrExpr: 239 err = visitTags(x.X, visit) 240 if err == nil { 241 err = visitTags(x.Y, visit) 242 } 243 244 default: 245 return fmt.Errorf("unknown constraint type: %T", x) 246 } 247 248 return 249 } 250 251 func collectTags(expr constraint.Expr) ([]string, error) { 252 var tags []string 253 err := visitTags(expr, func(tag string) { 254 tags = append(tags, tag) 255 }) 256 if err != nil { 257 return nil, err 258 } 259 260 return tags, err 261 } 262 263 // cgoTagsAndOpts contains compile or link options which should only be applied 264 // if the given set of build tags are satisfied. These options have already 265 // been tokenized using the same algorithm that "go build" uses, then joined 266 // with OptSeparator. 267 type cgoTagsAndOpts struct { 268 *buildTags 269 opts string 270 } 271 272 func (c *cgoTagsAndOpts) tags() []string { 273 if c == nil { 274 return nil 275 } 276 277 return c.buildTags.tags() 278 } 279 280 func (c *cgoTagsAndOpts) eval(ok func(string) bool) bool { 281 if c == nil { 282 return true 283 } 284 285 return c.buildTags.eval(ok) 286 } 287 288 // matchAuto interprets text as either a +build or //go:build expression (whichever works). 289 // Forked from go/build.Context.matchAuto 290 func matchAuto(tokens []string) (*buildTags, error) { 291 if len(tokens) == 0 { 292 return nil, nil 293 } 294 295 text := strings.Join(tokens, " ") 296 if strings.ContainsAny(text, "&|()") { 297 text = "//go:build " + text 298 } else { 299 text = "// +build " + text 300 } 301 302 x, err := constraint.Parse(text) 303 if err != nil { 304 return nil, err 305 } 306 307 return newBuildTags(x) 308 } 309 310 // isIgnoredTag returns whether the tag is "cgo", "purego", "race", "msan" or is a release tag. 311 // Release tags match the pattern "go[0-9]\.[0-9]+". 312 // Gazelle won't consider whether an ignored tag is satisfied when evaluating 313 // build constraints for a file and will instead defer to the compiler at compile 314 // time. 315 func isIgnoredTag(tag string) bool { 316 if tag == "cgo" || tag == "purego" || tag == "race" || tag == "msan" { 317 return true 318 } 319 if len(tag) < 5 || !strings.HasPrefix(tag, "go") { 320 return false 321 } 322 if tag[2] < '0' || tag[2] > '9' || tag[3] != '.' { 323 return false 324 } 325 for _, c := range tag[4:] { 326 if c < '0' || c > '9' { 327 return false 328 } 329 } 330 return true 331 } 332 333 // pushNot applies DeMorgan's law to push negations down the expression, 334 // so that only tags are negated in the result. 335 // (It applies the rewrites !(X && Y) => (!X || !Y) and !(X || Y) => (!X && !Y).) 336 // Forked from go/build/constraint.pushNot 337 func pushNot(x constraint.Expr, not bool) constraint.Expr { 338 switch x := x.(type) { 339 default: 340 // unreachable 341 return x 342 case *constraint.NotExpr: 343 if _, ok := x.X.(*constraint.TagExpr); ok && !not { 344 return x 345 } 346 return pushNot(x.X, !not) 347 case *constraint.TagExpr: 348 if not { 349 return &constraint.NotExpr{X: x} 350 } 351 return x 352 case *constraint.AndExpr: 353 x1 := pushNot(x.X, not) 354 y1 := pushNot(x.Y, not) 355 if not { 356 return or(x1, y1) 357 } 358 if x1 == x.X && y1 == x.Y { 359 return x 360 } 361 return and(x1, y1) 362 case *constraint.OrExpr: 363 x1 := pushNot(x.X, not) 364 y1 := pushNot(x.Y, not) 365 if not { 366 return and(x1, y1) 367 } 368 if x1 == x.X && y1 == x.Y { 369 return x 370 } 371 return or(x1, y1) 372 } 373 } 374 375 func or(x, y constraint.Expr) constraint.Expr { 376 return &constraint.OrExpr{X: x, Y: y} 377 } 378 379 func and(x, y constraint.Expr) constraint.Expr { 380 return &constraint.AndExpr{X: x, Y: y} 381 }