diff --git a/internal/loader/buildtag/expr.go b/internal/loader/buildtag/expr.go new file mode 100644 index 0000000000000000000000000000000000000000..f4f8032a9e8489d279bf8596b9942664711230a4 --- /dev/null +++ b/internal/loader/buildtag/expr.go @@ -0,0 +1,417 @@ +// 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 buildtag + +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 #wa: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 “#wa:build ...” +// and returns the corresponding boolean expression. +func Parse(line string) (Expr, error) { + if text, ok := splitWaBuild(line); ok { + return parseExpr(text) + } + return nil, errNotConstraint +} + +// IsWaBuild reports whether the line of text is a “#wa:build” constraint. +// It only checks the prefix of the text, not that the expression itself parses. +func IsWaBuild(line string) bool { + _, ok := splitWaBuild(line) + return ok +} + +// splitWaBuild splits apart the leading #wa:build prefix in line from the build expression itself. +// It returns "", false if the input is not a #wa:build line or if the input contains multiple lines. +func splitWaBuild(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, "#wa:build") { + return "", false + } + + line = strings.TrimSpace(line) + line = line[len("#wa:build"):] + + // If strings.TrimSpace finds more to trim after removing the #wa:build prefix, + // it means that the prefix was followed by a space, making this a #wa:build line + // (as opposed to a #wa:buildsomethingelse line). + // If line is empty, we had "#wa: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 +} + +// 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 +} + +// 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) +} diff --git a/internal/loader/buildtag/expr_test.go b/internal/loader/buildtag/expr_test.go new file mode 100644 index 0000000000000000000000000000000000000000..57aeacb5b52a7e9536f36e6dfe3eb058efdd282c --- /dev/null +++ b/internal/loader/buildtag/expr_test.go @@ -0,0 +1,270 @@ +// 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 buildtag + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +var exprStringTests = []struct { + x Expr + out string +}{ + { + x: tag("abc"), + out: "abc", + }, + { + x: not(tag("abc")), + out: "!abc", + }, + { + x: not(and(tag("abc"), tag("def"))), + out: "!(abc && def)", + }, + { + x: and(tag("abc"), or(tag("def"), tag("ghi"))), + out: "abc && (def || ghi)", + }, + { + x: or(and(tag("abc"), tag("def")), tag("ghi")), + out: "(abc && def) || ghi", + }, +} + +func TestExprString(t *testing.T) { + for i, tt := range exprStringTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + s := tt.x.String() + if s != tt.out { + t.Errorf("String() mismatch:\nhave %s\nwant %s", s, tt.out) + } + }) + } +} + +var lexTests = []struct { + in string + out string +}{ + {"", ""}, + {"x", "x"}, + {"x.y", "x.y"}, + {"x_y", "x_y"}, + {"αx", "αx"}, + {"αx²", "αx err: invalid syntax at ²"}, + {"go1.2", "go1.2"}, + {"x y", "x y"}, + {"x!y", "x ! y"}, + {"&&||!()xy yx ", "&& || ! ( ) xy yx"}, + {"x~", "x err: invalid syntax at ~"}, + {"x ~", "x err: invalid syntax at ~"}, + {"x &", "x err: invalid syntax at &"}, + {"x &y", "x err: invalid syntax at &"}, +} + +func TestLex(t *testing.T) { + for i, tt := range lexTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + p := &exprParser{s: tt.in} + out := "" + for { + tok, err := lexHelp(p) + if tok == "" && err == nil { + break + } + if out != "" { + out += " " + } + if err != nil { + out += "err: " + err.Error() + break + } + out += tok + } + if out != tt.out { + t.Errorf("lex(%q):\nhave %s\nwant %s", tt.in, out, tt.out) + } + }) + } +} + +func lexHelp(p *exprParser) (tok string, err error) { + defer func() { + if e := recover(); e != nil { + if e, ok := e.(*SyntaxError); ok { + err = e + return + } + panic(e) + } + }() + + p.lex() + return p.tok, nil +} + +var parseExprTests = []struct { + in string + x Expr +}{ + {"x", tag("x")}, + {"x&&y", and(tag("x"), tag("y"))}, + {"x||y", or(tag("x"), tag("y"))}, + {"(x)", tag("x")}, + {"x||y&&z", or(tag("x"), and(tag("y"), tag("z")))}, + {"x&&y||z", or(and(tag("x"), tag("y")), tag("z"))}, + {"x&&(y||z)", and(tag("x"), or(tag("y"), tag("z")))}, + {"(x||y)&&z", and(or(tag("x"), tag("y")), tag("z"))}, + {"!(x&&y)", not(and(tag("x"), tag("y")))}, +} + +func TestParseExpr(t *testing.T) { + for i, tt := range parseExprTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + x, err := parseExpr(tt.in) + if err != nil { + t.Fatal(err) + } + if x.String() != tt.x.String() { + t.Errorf("parseExpr(%q):\nhave %s\nwant %s", tt.in, x, tt.x) + } + }) + } +} + +var parseExprErrorTests = []struct { + in string + err error +}{ + {"x && ", &SyntaxError{Offset: 5, Err: "unexpected end of expression"}}, + {"x && (", &SyntaxError{Offset: 6, Err: "missing close paren"}}, + {"x && ||", &SyntaxError{Offset: 5, Err: "unexpected token ||"}}, + {"x && !", &SyntaxError{Offset: 6, Err: "unexpected end of expression"}}, + {"x && !!", &SyntaxError{Offset: 6, Err: "double negation not allowed"}}, + {"x !", &SyntaxError{Offset: 2, Err: "unexpected token !"}}, + {"x && (y", &SyntaxError{Offset: 5, Err: "missing close paren"}}, +} + +func TestParseError(t *testing.T) { + for i, tt := range parseExprErrorTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + x, err := parseExpr(tt.in) + if err == nil { + t.Fatalf("parseExpr(%q) = %v, want error", tt.in, x) + } + if !reflect.DeepEqual(err, tt.err) { + t.Fatalf("parseExpr(%q): wrong error:\nhave %#v\nwant %#v", tt.in, err, tt.err) + } + }) + } +} + +var exprEvalTests = []struct { + in string + ok bool + tags string +}{ + {"x", false, "x"}, + {"x && y", false, "x y"}, + {"x || y", false, "x y"}, + {"!x && yes", true, "x yes"}, + {"yes || y", true, "y yes"}, +} + +func TestExprEval(t *testing.T) { + for i, tt := range exprEvalTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + x, err := parseExpr(tt.in) + if err != nil { + t.Fatal(err) + } + tags := make(map[string]bool) + wantTags := make(map[string]bool) + for _, tag := range strings.Fields(tt.tags) { + wantTags[tag] = true + } + hasTag := func(tag string) bool { + tags[tag] = true + return tag == "yes" + } + ok := x.Eval(hasTag) + if ok != tt.ok || !reflect.DeepEqual(tags, wantTags) { + t.Errorf("Eval(%#q):\nhave ok=%v, tags=%v\nwant ok=%v, tags=%v", + tt.in, ok, tags, tt.ok, wantTags) + } + }) + } +} + +var parsePlusBuildExprTests = []struct { + in string + x Expr +}{ + {"x", tag("x")}, + {"x,y", and(tag("x"), tag("y"))}, + {"x y", or(tag("x"), tag("y"))}, + {"x y,z", or(tag("x"), and(tag("y"), tag("z")))}, + {"x,y z", or(and(tag("x"), tag("y")), tag("z"))}, + {"x,!y !z", or(and(tag("x"), not(tag("y"))), not(tag("z")))}, + {"!! x", or(tag("ignore"), tag("x"))}, + {"!!x", tag("ignore")}, + {"!x", not(tag("x"))}, + {"!", tag("ignore")}, + {"", tag("ignore")}, +} + +var constraintTests = []struct { + in string + x Expr + err string +}{ + {"#wa:build x && y", and(tag("x"), tag("y")), ""}, + {"#wa:build x && y\n", and(tag("x"), tag("y")), ""}, + {"#wa:build x && y\n ", nil, "not a build constraint"}, + {"#wa:build x && y\nmore", nil, "not a build constraint"}, + {" #wa:build x && y", nil, "not a build constraint"}, + {"#wa:build\n", nil, "unexpected end of expression"}, +} + +func TestParse(t *testing.T) { + for i, tt := range constraintTests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + x, err := Parse(tt.in) + if err != nil { + if tt.err == "" { + t.Errorf("Constraint(%q): unexpected error: %v", tt.in, err) + } else if !strings.Contains(err.Error(), tt.err) { + t.Errorf("Constraint(%q): error %v, want %v", tt.in, err, tt.err) + } + return + } + if tt.err != "" { + t.Errorf("Constraint(%q) = %v, want error %v", tt.in, x, tt.err) + return + } + if x.String() != tt.x.String() { + t.Errorf("Constraint(%q):\nhave %v\nwant %v", tt.in, x, tt.x) + } + }) + } +} + +var plusBuildLinesTests = []struct { + in string + out []string + err error +}{ + {"x", []string{"x"}, nil}, + {"x && !y", []string{"x,!y"}, nil}, + {"x || y", []string{"x y"}, nil}, + {"x && (y || z)", []string{"x", "y z"}, nil}, + {"!(x && y)", []string{"!x !y"}, nil}, + {"x || (y && z)", []string{"x y,z"}, nil}, + {"w && (x || (y && z))", []string{"w", "x y,z"}, nil}, +}