| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package constraint implements parsing and evaluation of build constraint lines. |
| // See https://golang.org/cmd/go/#hdr-Build_constraints for documentation about build constraints themselves. |
| // |
| // This package parses both the original “// +build” syntax and the “//go:build” syntax that will be added in Go 1.17. |
| // The parser is being included in Go 1.16 to allow tools that need to process Go 1.17 source code |
| // to still be built against the Go 1.16 release. |
| // See https://golang.org/design/draft-gobuild for details about the “//go:build” syntax. |
| package constraint |
| |
| import ( |
| "errors" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| ) |
| |
| // An Expr is a build tag constraint expression. |
| // The underlying concrete type is *AndExpr, *OrExpr, *NotExpr, or *TagExpr. |
| type Expr interface { |
| // String returns the string form of the expression, |
| // using the boolean syntax used in //go:build lines. |
| String() string |
| |
| // Eval reports whether the expression evaluates to true. |
| // It calls ok(tag) as needed to find out whether a given build tag |
| // is satisfied by the current build configuration. |
| Eval(ok func(tag string) bool) bool |
| |
| // The presence of an isExpr method explicitly marks the type as an Expr. |
| // Only implementations in this package should be used as Exprs. |
| isExpr() |
| } |
| |
| // A TagExpr is an Expr for the single tag Tag. |
| type TagExpr struct { |
| Tag string // for example, “linux” or “cgo” |
| } |
| |
| func (x *TagExpr) isExpr() {} |
| |
| func (x *TagExpr) Eval(ok func(tag string) bool) bool { |
| return ok(x.Tag) |
| } |
| |
| func (x *TagExpr) String() string { |
| return x.Tag |
| } |
| |
| func tag(tag string) Expr { return &TagExpr{tag} } |
| |
| // A NotExpr represents the expression !X (the negation of X). |
| type NotExpr struct { |
| X Expr |
| } |
| |
| func (x *NotExpr) isExpr() {} |
| |
| func (x *NotExpr) Eval(ok func(tag string) bool) bool { |
| return !x.X.Eval(ok) |
| } |
| |
| func (x *NotExpr) String() string { |
| s := x.X.String() |
| switch x.X.(type) { |
| case *AndExpr, *OrExpr: |
| s = "(" + s + ")" |
| } |
| return "!" + s |
| } |
| |
| func not(x Expr) Expr { return &NotExpr{x} } |
| |
| // An AndExpr represents the expression X && Y. |
| type AndExpr struct { |
| X, Y Expr |
| } |
| |
| func (x *AndExpr) isExpr() {} |
| |
| func (x *AndExpr) Eval(ok func(tag string) bool) bool { |
| // Note: Eval both, to make sure ok func observes all tags. |
| xok := x.X.Eval(ok) |
| yok := x.Y.Eval(ok) |
| return xok && yok |
| } |
| |
| func (x *AndExpr) String() string { |
| return andArg(x.X) + " && " + andArg(x.Y) |
| } |
| |
| func andArg(x Expr) string { |
| s := x.String() |
| if _, ok := x.(*OrExpr); ok { |
| s = "(" + s + ")" |
| } |
| return s |
| } |
| |
| func and(x, y Expr) Expr { |
| return &AndExpr{x, y} |
| } |
| |
| // An OrExpr represents the expression X || Y. |
| type OrExpr struct { |
| X, Y Expr |
| } |
| |
| func (x *OrExpr) isExpr() {} |
| |
| func (x *OrExpr) Eval(ok func(tag string) bool) bool { |
| // Note: Eval both, to make sure ok func observes all tags. |
| xok := x.X.Eval(ok) |
| yok := x.Y.Eval(ok) |
| return xok || yok |
| } |
| |
| func (x *OrExpr) String() string { |
| return orArg(x.X) + " || " + orArg(x.Y) |
| } |
| |
| func orArg(x Expr) string { |
| s := x.String() |
| if _, ok := x.(*AndExpr); ok { |
| s = "(" + s + ")" |
| } |
| return s |
| } |
| |
| func or(x, y Expr) Expr { |
| return &OrExpr{x, y} |
| } |
| |
| // A SyntaxError reports a syntax error in a parsed build expression. |
| type SyntaxError struct { |
| Offset int // byte offset in input where error was detected |
| Err string // description of error |
| } |
| |
| func (e *SyntaxError) Error() string { |
| return e.Err |
| } |
| |
| var errNotConstraint = errors.New("not a build constraint") |
| |
| // Parse parses a single build constraint line of the form “//go:build ...” or “// +build ...” |
| // and returns the corresponding boolean expression. |
| func Parse(line string) (Expr, error) { |
| if text, ok := splitGoBuild(line); ok { |
| return parseExpr(text) |
| } |
| if text, ok := splitPlusBuild(line); ok { |
| return parsePlusBuildExpr(text), nil |
| } |
| return nil, errNotConstraint |
| } |
| |
| // IsGoBuild reports whether the line of text is a “//go:build” constraint. |
| // It only checks the prefix of the text, not that the expression itself parses. |
| func IsGoBuild(line string) bool { |
| _, ok := splitGoBuild(line) |
| return ok |
| } |
| |
| // splitGoBuild splits apart the leading //go:build prefix in line from the build expression itself. |
| // It returns "", false if the input is not a //go:build line or if the input contains multiple lines. |
| func splitGoBuild(line string) (expr string, ok bool) { |
| // A single trailing newline is OK; otherwise multiple lines are not. |
| if len(line) > 0 && line[len(line)-1] == '\n' { |
| line = line[:len(line)-1] |
| } |
| if strings.Contains(line, "\n") { |
| return "", false |
| } |
| |
| if !strings.HasPrefix(line, "//go:build") { |
| return "", false |
| } |
| |
| line = strings.TrimSpace(line) |
| line = line[len("//go:build"):] |
| |
| // If strings.TrimSpace finds more to trim after removing the //go:build prefix, |
| // it means that the prefix was followed by a space, making this a //go:build line |
| // (as opposed to a //go:buildsomethingelse line). |
| // If line is empty, we had "//go:build" by itself, which also counts. |
| trim := strings.TrimSpace(line) |
| if len(line) == len(trim) && line != "" { |
| return "", false |
| } |
| |
| return trim, true |
| } |
| |
| // An exprParser holds state for parsing a build expression. |
| type exprParser struct { |
| s string // input string |
| i int // next read location in s |
| |
| tok string // last token read |
| isTag bool |
| pos int // position (start) of last token |
| } |
| |
| // parseExpr parses a boolean build tag expression. |
| func parseExpr(text string) (x Expr, err error) { |
| defer func() { |
| if e := recover(); e != nil { |
| if e, ok := e.(*SyntaxError); ok { |
| err = e |
| return |
| } |
| panic(e) // unreachable unless parser has a bug |
| } |
| }() |
| |
| p := &exprParser{s: text} |
| x = p.or() |
| if p.tok != "" { |
| panic(&SyntaxError{Offset: p.pos, Err: "unexpected token " + p.tok}) |
| } |
| return x, nil |
| } |
| |
| // or parses a sequence of || expressions. |
| // On entry, the next input token has not yet been lexed. |
| // On exit, the next input token has been lexed and is in p.tok. |
| func (p *exprParser) or() Expr { |
| x := p.and() |
| for p.tok == "||" { |
| x = or(x, p.and()) |
| } |
| return x |
| } |
| |
| // and parses a sequence of && expressions. |
| // On entry, the next input token has not yet been lexed. |
| // On exit, the next input token has been lexed and is in p.tok. |
| func (p *exprParser) and() Expr { |
| x := p.not() |
| for p.tok == "&&" { |
| x = and(x, p.not()) |
| } |
| return x |
| } |
| |
| // not parses a ! expression. |
| // On entry, the next input token has not yet been lexed. |
| // On exit, the next input token has been lexed and is in p.tok. |
| func (p *exprParser) not() Expr { |
| p.lex() |
| if p.tok == "!" { |
| p.lex() |
| if p.tok == "!" { |
| panic(&SyntaxError{Offset: p.pos, Err: "double negation not allowed"}) |
| } |
| return not(p.atom()) |
| } |
| return p.atom() |
| } |
| |
| // atom parses a tag or a parenthesized expression. |
| // On entry, the next input token HAS been lexed. |
| // On exit, the next input token has been lexed and is in p.tok. |
| func (p *exprParser) atom() Expr { |
| // first token already in p.tok |
| if p.tok == "(" { |
| pos := p.pos |
| defer func() { |
| if e := recover(); e != nil { |
| if e, ok := e.(*SyntaxError); ok && e.Err == "unexpected end of expression" { |
| e.Err = "missing close paren" |
| } |
| panic(e) |
| } |
| }() |
| x := p.or() |
| if p.tok != ")" { |
| panic(&SyntaxError{Offset: pos, Err: "missing close paren"}) |
| } |
| p.lex() |
| return x |
| } |
| |
| if !p.isTag { |
| if p.tok == "" { |
| panic(&SyntaxError{Offset: p.pos, Err: "unexpected end of expression"}) |
| } |
| panic(&SyntaxError{Offset: p.pos, Err: "unexpected token " + p.tok}) |
| } |
| tok := p.tok |
| p.lex() |
| return tag(tok) |
| } |
| |
| // lex finds and consumes the next token in the input stream. |
| // On return, p.tok is set to the token text, |
| // p.isTag reports whether the token was a tag, |
| // and p.pos records the byte offset of the start of the token in the input stream. |
| // If lex reaches the end of the input, p.tok is set to the empty string. |
| // For any other syntax error, lex panics with a SyntaxError. |
| func (p *exprParser) lex() { |
| p.isTag = false |
| for p.i < len(p.s) && (p.s[p.i] == ' ' || p.s[p.i] == '\t') { |
| p.i++ |
| } |
| if p.i >= len(p.s) { |
| p.tok = "" |
| p.pos = p.i |
| return |
| } |
| switch p.s[p.i] { |
| case '(', ')', '!': |
| p.pos = p.i |
| p.i++ |
| p.tok = p.s[p.pos:p.i] |
| return |
| |
| case '&', '|': |
| if p.i+1 >= len(p.s) || p.s[p.i+1] != p.s[p.i] { |
| panic(&SyntaxError{Offset: p.i, Err: "invalid syntax at " + string(rune(p.s[p.i]))}) |
| } |
| p.pos = p.i |
| p.i += 2 |
| p.tok = p.s[p.pos:p.i] |
| return |
| } |
| |
| tag := p.s[p.i:] |
| for i, c := range tag { |
| if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { |
| tag = tag[:i] |
| break |
| } |
| } |
| if tag == "" { |
| c, _ := utf8.DecodeRuneInString(p.s[p.i:]) |
| panic(&SyntaxError{Offset: p.i, Err: "invalid syntax at " + string(c)}) |
| } |
| |
| p.pos = p.i |
| p.i += len(tag) |
| p.tok = p.s[p.pos:p.i] |
| p.isTag = true |
| return |
| } |
| |
| // IsPlusBuild reports whether the line of text is a “// +build” constraint. |
| // It only checks the prefix of the text, not that the expression itself parses. |
| func IsPlusBuild(line string) bool { |
| _, ok := splitPlusBuild(line) |
| return ok |
| } |
| |
| // splitGoBuild splits apart the leading //go:build prefix in line from the build expression itself. |
| // It returns "", false if the input is not a //go:build line or if the input contains multiple lines. |
| func splitPlusBuild(line string) (expr string, ok bool) { |
| // A single trailing newline is OK; otherwise multiple lines are not. |
| if len(line) > 0 && line[len(line)-1] == '\n' { |
| line = line[:len(line)-1] |
| } |
| if strings.Contains(line, "\n") { |
| return "", false |
| } |
| |
| if !strings.HasPrefix(line, "//") { |
| return "", false |
| } |
| line = line[len("//"):] |
| // Note the space is optional; "//+build" is recognized too. |
| line = strings.TrimSpace(line) |
| |
| if !strings.HasPrefix(line, "+build") { |
| return "", false |
| } |
| line = line[len("+build"):] |
| |
| // If strings.TrimSpace finds more to trim after removing the +build prefix, |
| // it means that the prefix was followed by a space, making this a +build line |
| // (as opposed to a +buildsomethingelse line). |
| // If line is empty, we had "// +build" by itself, which also counts. |
| trim := strings.TrimSpace(line) |
| if len(line) == len(trim) && line != "" { |
| return "", false |
| } |
| |
| return trim, true |
| } |
| |
| // parsePlusBuildExpr parses a legacy build tag expression (as used with “// +build”). |
| func parsePlusBuildExpr(text string) Expr { |
| var x Expr |
| for _, clause := range strings.Fields(text) { |
| var y Expr |
| for _, lit := range strings.Split(clause, ",") { |
| var z Expr |
| var neg bool |
| if strings.HasPrefix(lit, "!!") || lit == "!" { |
| z = tag("ignore") |
| } else { |
| if strings.HasPrefix(lit, "!") { |
| neg = true |
| lit = lit[len("!"):] |
| } |
| if isValidTag(lit) { |
| z = tag(lit) |
| } else { |
| z = tag("ignore") |
| } |
| if neg { |
| z = not(z) |
| } |
| } |
| if y == nil { |
| y = z |
| } else { |
| y = and(y, z) |
| } |
| } |
| if x == nil { |
| x = y |
| } else { |
| x = or(x, y) |
| } |
| } |
| return x |
| } |
| |
| // isValidTag reports whether the word is a valid build tag. |
| // Tags must be letters, digits, underscores or dots. |
| // Unlike in Go identifiers, all digits are fine (e.g., "386"). |
| func isValidTag(word string) bool { |
| if word == "" { |
| return false |
| } |
| for _, c := range word { |
| if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { |
| return false |
| } |
| } |
| return true |
| } |
| |
| var errComplex = errors.New("expression too complex for // +build lines") |
| |
| // PlusBuildLines returns a sequence of “// +build” lines that evaluate to the build expression x. |
| // If the expression is too complex to convert directly to “// +build” lines, PlusBuildLines returns an error. |
| func PlusBuildLines(x Expr) ([]string, error) { |
| // Push all NOTs to the expression leaves, so that //go:build !(x && y) can be treated as !x || !y. |
| // This rewrite is both efficient and commonly needed, so it's worth doing. |
| // Essentially all other possible rewrites are too expensive and too rarely needed. |
| x = pushNot(x, false) |
| |
| // Split into AND of ORs of ANDs of literals (tag or NOT tag). |
| var split [][][]Expr |
| for _, or := range appendSplitAnd(nil, x) { |
| var ands [][]Expr |
| for _, and := range appendSplitOr(nil, or) { |
| var lits []Expr |
| for _, lit := range appendSplitAnd(nil, and) { |
| switch lit.(type) { |
| case *TagExpr, *NotExpr: |
| lits = append(lits, lit) |
| default: |
| return nil, errComplex |
| } |
| } |
| ands = append(ands, lits) |
| } |
| split = append(split, ands) |
| } |
| |
| // If all the ORs have length 1 (no actual OR'ing going on), |
| // push the top-level ANDs to the bottom level, so that we get |
| // one // +build line instead of many. |
| maxOr := 0 |
| for _, or := range split { |
| if maxOr < len(or) { |
| maxOr = len(or) |
| } |
| } |
| if maxOr == 1 { |
| var lits []Expr |
| for _, or := range split { |
| lits = append(lits, or[0]...) |
| } |
| split = [][][]Expr{{lits}} |
| } |
| |
| // Prepare the +build lines. |
| var lines []string |
| for _, or := range split { |
| line := "// +build" |
| for _, and := range or { |
| clause := "" |
| for i, lit := range and { |
| if i > 0 { |
| clause += "," |
| } |
| clause += lit.String() |
| } |
| line += " " + clause |
| } |
| lines = append(lines, line) |
| } |
| |
| return lines, nil |
| } |
| |
| // 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).) |
| func pushNot(x Expr, not bool) Expr { |
| switch x := x.(type) { |
| default: |
| // unreachable |
| return x |
| case *NotExpr: |
| if _, ok := x.X.(*TagExpr); ok && !not { |
| return x |
| } |
| return pushNot(x.X, !not) |
| case *TagExpr: |
| if not { |
| return &NotExpr{X: x} |
| } |
| return x |
| case *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 *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) |
| } |
| } |
| |
| // appendSplitAnd appends x to list while splitting apart any top-level && expressions. |
| // For example, appendSplitAnd({W}, X && Y && Z) = {W, X, Y, Z}. |
| func appendSplitAnd(list []Expr, x Expr) []Expr { |
| if x, ok := x.(*AndExpr); ok { |
| list = appendSplitAnd(list, x.X) |
| list = appendSplitAnd(list, x.Y) |
| return list |
| } |
| return append(list, x) |
| } |
| |
| // appendSplitOr appends x to list while splitting apart any top-level || expressions. |
| // For example, appendSplitOr({W}, X || Y || Z) = {W, X, Y, Z}. |
| func appendSplitOr(list []Expr, x Expr) []Expr { |
| if x, ok := x.(*OrExpr); ok { |
| list = appendSplitOr(list, x.X) |
| list = appendSplitOr(list, x.Y) |
| return list |
| } |
| return append(list, x) |
| } |