| /* 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 golang |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "go/build/constraint" |
| "os" |
| "strings" |
| ) |
| |
| // readTags reads and extracts build tags from the block of comments |
| // and blank lines at the start of a file which is separated from the |
| // rest of the file by a blank line. Each string in the returned slice |
| // is the trimmed text of a line after a "+build" prefix. |
| // Based on go/build.Context.shouldBuild. |
| func readTags(path string) (*buildTags, error) { |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| |
| content, err := readComments(f) |
| if err != nil { |
| return nil, err |
| } |
| |
| content, goBuild, _, err := parseFileHeader(content) |
| if err != nil { |
| return nil, err |
| } |
| |
| if goBuild != nil { |
| x, err := constraint.Parse(string(goBuild)) |
| if err != nil { |
| return nil, err |
| } |
| |
| return newBuildTags(x), nil |
| } |
| |
| var fullConstraint constraint.Expr |
| // Search and parse +build tags |
| scanner := bufio.NewScanner(bytes.NewReader(content)) |
| for scanner.Scan() { |
| line := strings.TrimSpace(scanner.Text()) |
| |
| if !constraint.IsPlusBuild(line) { |
| continue |
| } |
| |
| x, err := constraint.Parse(line) |
| if err != nil { |
| return nil, err |
| } |
| |
| if fullConstraint != nil { |
| fullConstraint = &constraint.AndExpr{ |
| X: fullConstraint, |
| Y: x, |
| } |
| } else { |
| fullConstraint = x |
| } |
| } |
| |
| if scanner.Err() != nil { |
| return nil, scanner.Err() |
| } |
| |
| if fullConstraint == nil { |
| return nil, nil |
| } |
| |
| return newBuildTags(fullConstraint), nil |
| } |
| |
| // buildTags represents the build tags specified in a file. |
| type buildTags struct { |
| // expr represents the parsed constraint expression |
| // that can be used to evaluate a file against a set |
| // of tags. |
| expr constraint.Expr |
| // rawTags represents the concrete tags that make up expr. |
| rawTags []string |
| } |
| |
| // newBuildTags will return a new buildTags structure with any |
| // ignored tags filtered out from the provided constraints. |
| func newBuildTags(x constraint.Expr) *buildTags { |
| modified := dropNegationForIgnoredTags(pushNot(x, false), isDefaultIgnoredTag) |
| rawTags := collectTags(modified) |
| |
| return &buildTags{ |
| expr: modified, |
| rawTags: rawTags, |
| } |
| } |
| |
| func (b *buildTags) tags() []string { |
| if b == nil { |
| return nil |
| } |
| |
| return b.rawTags |
| } |
| |
| func (b *buildTags) eval(ok func(string) bool) bool { |
| if b == nil || b.expr == nil { |
| return true |
| } |
| |
| return b.expr.Eval(ok) |
| } |
| |
| func (b *buildTags) empty() bool { |
| if b == nil { |
| return true |
| } |
| |
| return len(b.rawTags) == 0 |
| } |
| |
| // dropNegationForIgnoredTags drops negations for any concrete tags that should be ignored. |
| // This is done to ensure that when ignored tags are evaluated, they can always return true |
| // without having to worry that the result will be negated later on. Ignored tags should always |
| // evaluate to true, regardless of whether they are negated or not leaving the final evaluation |
| // to happen at compile time by the compiler. |
| func dropNegationForIgnoredTags(expr constraint.Expr, isIgnoredTag func(tag string) bool) constraint.Expr { |
| if expr == nil { |
| return nil |
| } |
| |
| switch x := expr.(type) { |
| case *constraint.TagExpr: |
| return &constraint.TagExpr{ |
| Tag: x.Tag, |
| } |
| |
| case *constraint.NotExpr: |
| var toRet constraint.Expr |
| // flip nots on any ignored tags |
| if tag, ok := x.X.(*constraint.TagExpr); ok && isIgnoredTag(tag.Tag) { |
| toRet = &constraint.TagExpr{ |
| Tag: tag.Tag, |
| } |
| } else { |
| fixed := dropNegationForIgnoredTags(x.X, isIgnoredTag) |
| toRet = &constraint.NotExpr{X: fixed} |
| } |
| |
| return toRet |
| |
| case *constraint.AndExpr: |
| a := dropNegationForIgnoredTags(x.X, isIgnoredTag) |
| b := dropNegationForIgnoredTags(x.Y, isIgnoredTag) |
| |
| return &constraint.AndExpr{ |
| X: a, |
| Y: b, |
| } |
| |
| case *constraint.OrExpr: |
| a := dropNegationForIgnoredTags(x.X, isIgnoredTag) |
| b := dropNegationForIgnoredTags(x.Y, isIgnoredTag) |
| return &constraint.OrExpr{ |
| X: a, |
| Y: b, |
| } |
| |
| default: |
| panic(fmt.Errorf("unknown constraint type: %T", x)) |
| } |
| } |
| |
| // visitTags will traverse the provided constraint.Expr, recursively, and call |
| // the user provided ok func on concrete constraint.TagExpr structures. If the provided |
| // func returns true, the tag in question is kept, otherwise it is filtered out. |
| func visitTags(expr constraint.Expr, visit func(string)) { |
| if expr == nil { |
| return |
| } |
| |
| switch x := expr.(type) { |
| case *constraint.TagExpr: |
| visit(x.Tag) |
| |
| case *constraint.NotExpr: |
| visitTags(x.X, visit) |
| |
| case *constraint.AndExpr: |
| visitTags(x.X, visit) |
| visitTags(x.Y, visit) |
| |
| case *constraint.OrExpr: |
| visitTags(x.X, visit) |
| visitTags(x.Y, visit) |
| |
| default: |
| panic(fmt.Errorf("unknown constraint type: %T", x)) |
| } |
| |
| return |
| } |
| |
| func collectTags(expr constraint.Expr) []string { |
| var tags []string |
| visitTags(expr, func(tag string) { |
| tags = append(tags, tag) |
| }) |
| return tags |
| } |
| |
| // cgoTagsAndOpts contains compile or link options which should only be applied |
| // if the given set of build tags are satisfied. These options have already |
| // been tokenized using the same algorithm that "go build" uses, then joined |
| // with OptSeparator. |
| type cgoTagsAndOpts struct { |
| *buildTags |
| opts string |
| } |
| |
| func (c *cgoTagsAndOpts) tags() []string { |
| if c == nil { |
| return nil |
| } |
| |
| return c.buildTags.tags() |
| } |
| |
| func (c *cgoTagsAndOpts) eval(ok func(string) bool) bool { |
| if c == nil { |
| return true |
| } |
| |
| return c.buildTags.eval(ok) |
| } |
| |
| // matchAuto interprets text as either a +build or //go:build expression (whichever works). |
| // Forked from go/build.Context.matchAuto |
| func matchAuto(tokens []string) (*buildTags, error) { |
| if len(tokens) == 0 { |
| return nil, nil |
| } |
| |
| text := strings.Join(tokens, " ") |
| if strings.ContainsAny(text, "&|()") { |
| text = "//go:build " + text |
| } else { |
| text = "// +build " + text |
| } |
| |
| x, err := constraint.Parse(text) |
| if err != nil { |
| return nil, err |
| } |
| |
| return newBuildTags(x), nil |
| } |
| |
| // isDefaultIgnoredTag returns whether the tag is "cgo", "purego", "race", "msan" or is a release tag. |
| // Release tags match the pattern "go[0-9]\.[0-9]+". |
| // Gazelle won't consider whether an ignored tag is satisfied when evaluating |
| // build constraints for a file and will instead defer to the compiler at compile |
| // time. |
| func isDefaultIgnoredTag(tag string) bool { |
| if tag == "cgo" || tag == "purego" || tag == "race" || tag == "msan" { |
| return true |
| } |
| if len(tag) < 5 || !strings.HasPrefix(tag, "go") { |
| return false |
| } |
| if tag[2] < '0' || tag[2] > '9' || tag[3] != '.' { |
| return false |
| } |
| for _, c := range tag[4:] { |
| if c < '0' || c > '9' { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // pushNot applies DeMorgan's law to push negations down the expression, |
| // so that only tags are negated in the result. |
| // (It applies the rewrites !(X && Y) => (!X || !Y) and !(X || Y) => (!X && !Y).) |
| // Forked from go/build/constraint.pushNot |
| func pushNot(x constraint.Expr, not bool) constraint.Expr { |
| switch x := x.(type) { |
| default: |
| // unreachable |
| return x |
| case *constraint.NotExpr: |
| if _, ok := x.X.(*constraint.TagExpr); ok && !not { |
| return x |
| } |
| return pushNot(x.X, !not) |
| case *constraint.TagExpr: |
| if not { |
| return &constraint.NotExpr{X: x} |
| } |
| return x |
| case *constraint.AndExpr: |
| x1 := pushNot(x.X, not) |
| y1 := pushNot(x.Y, not) |
| if not { |
| return or(x1, y1) |
| } |
| if x1 == x.X && y1 == x.Y { |
| return x |
| } |
| return and(x1, y1) |
| case *constraint.OrExpr: |
| x1 := pushNot(x.X, not) |
| y1 := pushNot(x.Y, not) |
| if not { |
| return and(x1, y1) |
| } |
| if x1 == x.X && y1 == x.Y { |
| return x |
| } |
| return or(x1, y1) |
| } |
| } |
| |
| func or(x, y constraint.Expr) constraint.Expr { |
| return &constraint.OrExpr{X: x, Y: y} |
| } |
| |
| func and(x, y constraint.Expr) constraint.Expr { |
| return &constraint.AndExpr{X: x, Y: y} |
| } |