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  }