From beeb9ed525785cf5d6a13fa9db052410e9297c5e Mon Sep 17 00:00:00 2001 From: Michael McLoughlin Date: Tue, 1 Jan 2019 22:55:57 -0800 Subject: [PATCH] buildtags: package for build tag representation Updates #3 --- buildtags/buildtags.go | 204 ++++++++++++++++++++++++++++++++++++ buildtags/buildtags_test.go | 86 +++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 buildtags/buildtags.go create mode 100644 buildtags/buildtags_test.go diff --git a/buildtags/buildtags.go b/buildtags/buildtags.go new file mode 100644 index 0000000..977c6d2 --- /dev/null +++ b/buildtags/buildtags.go @@ -0,0 +1,204 @@ +package buildtags + +import ( + "errors" + "fmt" + "strings" + "unicode" +) + +// Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/go/build/doc.go#L73-L92 +// +// // A build constraint is evaluated as the OR of space-separated options; +// // each option evaluates as the AND of its comma-separated terms; +// // and each term is an alphanumeric word or, preceded by !, its negation. +// // That is, the build constraint: +// // +// // // +build linux,386 darwin,!cgo +// // +// // corresponds to the boolean formula: +// // +// // (linux AND 386) OR (darwin AND (NOT cgo)) +// // +// // A file may have multiple build constraints. The overall constraint is the AND +// // of the individual constraints. That is, the build constraints: +// // +// // // +build linux darwin +// // // +build 386 +// // +// // corresponds to the boolean formula: +// // +// // (linux OR darwin) AND 386 +// + +type Interface interface { + ConstraintsConvertable + fmt.GoStringer + Validate() error +} + +type ConstraintsConvertable interface { + ToConstraints() Constraints +} + +type ConstraintConvertable interface { + ToConstraint() Constraint +} + +type OptionConvertable interface { + ToOption() Option +} + +type ( + Constraints []Constraint + Constraint []Option + Option []Term + Term string +) + +func (cs Constraints) ToConstraints() Constraints { return cs } + +func (cs Constraints) Validate() error { + for _, c := range cs { + if err := c.Validate(); err != nil { + return err + } + } + return nil +} +func (cs Constraints) GoString() string { + s := "" + for _, c := range cs { + s += c.GoString() + } + return s +} + +func (c Constraint) ToConstraints() Constraints { return Constraints{c} } +func (c Constraint) ToConstraint() Constraint { return c } + +func (c Constraint) Validate() error { + for _, o := range c { + if err := o.Validate(); err != nil { + return err + } + } + return nil +} + +func (c Constraint) GoString() string { + s := "// +build" + for _, o := range c { + s += " " + o.GoString() + } + return s + "\n" +} + +func (o Option) ToConstraints() Constraints { return o.ToConstraint().ToConstraints() } +func (o Option) ToConstraint() Constraint { return Constraint{o} } +func (o Option) ToOption() Option { return o } + +func (o Option) Validate() error { + for _, t := range o { + if err := t.Validate(); err != nil { + return fmt.Errorf("invalid term \"%s\": %s", t, err) + } + } + return nil +} + +func (o Option) GoString() string { + var ts []string + for _, t := range o { + ts = append(ts, t.GoString()) + } + return strings.Join(ts, ",") +} + +func (t Term) ToConstraints() Constraints { return t.ToOption().ToConstraints() } +func (t Term) ToConstraint() Constraint { return t.ToOption().ToConstraint() } +func (t Term) ToOption() Option { return Option{t} } + +func (t Term) Validate() error { + name := string(t) + + // Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/cmd/go/internal/imports/build.go#L110-L112 + // + // if strings.HasPrefix(name, "!!") { // bad syntax, reject always + // return false + // } + // + if strings.HasPrefix(name, "!!") { + return errors.New("at most one '!' allowed") + } + + name = strings.TrimPrefix(name, "!") + + if len(name) == 0 { + return errors.New("empty tag name") + } + + // Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/cmd/go/internal/imports/build.go#L121-L127 + // + // // Tags must be letters, digits, underscores or dots. + // // Unlike in Go identifiers, all digits are fine (e.g., "386"). + // for _, c := range name { + // if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { + // return false + // } + // } + // + for _, c := range name { + if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { + return fmt.Errorf("character '%c' disallowed in tags", c) + } + } + + return nil +} + +func (t Term) GoString() string { return string(t) } + +func Not(ident string) Term { + return Term("!" + ident) +} + +func And(cs ...ConstraintConvertable) Constraints { + constraints := Constraints{} + for _, c := range cs { + constraints = append(constraints, c.ToConstraint()) + } + return constraints +} + +func Any(opts ...OptionConvertable) Constraint { + c := Constraint{} + for _, opt := range opts { + c = append(c, opt.ToOption()) + } + return c +} + +func Opt(terms ...Term) Option { + return Option(terms) +} + +func ParseOption(expr string) (Option, error) { + opt := Option{} + for _, t := range strings.Split(expr, ",") { + opt = append(opt, Term(t)) + } + return opt, opt.Validate() +} + +func ParseConstraint(expr string) (Constraint, error) { + c := Constraint{} + for _, field := range strings.Fields(expr) { + opt, err := ParseOption(field) + if err != nil { + return c, err + } + c = append(c, opt) + } + return c, nil +} diff --git a/buildtags/buildtags_test.go b/buildtags/buildtags_test.go new file mode 100644 index 0000000..4c11b5b --- /dev/null +++ b/buildtags/buildtags_test.go @@ -0,0 +1,86 @@ +package buildtags + +import "testing" + +func TestGoString(t *testing.T) { + cases := []struct { + Constraint Interface + Expect string + }{ + {Term("amd64"), "// +build amd64\n"}, + {Any(Opt(Term("linux"), Term("386")), Opt("darwin", Not("cgo"))), "// +build linux,386 darwin,!cgo\n"}, + {And(Any(Term("linux"), Term("darwin")), Term("386")), "// +build linux darwin\n// +build 386\n"}, + } + for _, c := range cases { + got := c.Constraint.ToConstraints().GoString() + if got != c.Expect { + t.Errorf("constraint %#v GoString() got %q; expected %q", c.Constraint, got, c.Expect) + } + } +} + +func TestValidateOK(t *testing.T) { + cases := []Interface{ + Term("name"), + Term("!name"), + } + for _, c := range cases { + if err := c.ToConstraints().Validate(); err != nil { + t.Errorf("unexpected validation error for %#v: %q", c, err) + } + } +} + +func TestValidateErrors(t *testing.T) { + cases := []struct { + Constraint Interface + ExpectMessage string + }{ + {Term(""), "empty tag name"}, + {Term("!"), "empty tag name"}, + {Term("!!"), "at most one '!' allowed"}, + {Term("!abc!def"), "character '!' disallowed in tags"}, + { + And(Any(Term("linux"), Term("my-os")), Term("386")).ToConstraints(), + "invalid term \"my-os\": character '-' disallowed in tags", + }, + } + for _, c := range cases { + err := c.Constraint.Validate() + if err == nil { + t.Fatalf("expect validation error for constraint:\n%s", c.Constraint.GoString()) + } + if err.Error() != c.ExpectMessage { + t.Fatalf("unexpected error message\n\tgot:\t%q\n\texpect:\t%q\n", err, c.ExpectMessage) + } + } +} + +func TestParseConstraintRoundTrip(t *testing.T) { + exprs := []string{ + "amd64", + "amd64,linux", + "!a", + "!a,b c,!d,e", + "linux,386 darwin,!cgo", + } + for _, expr := range exprs { + c, err := ParseConstraint(expr) + if err != nil { + t.Fatalf("error parsing expression %q: %q", expr, err) + } + got := c.GoString() + expect := "// +build " + expr + "\n" + if got != expect { + t.Fatalf("roundtrip error\n\tgot\t%q\n\texpect\t%q\n", got, expect) + } + } +} + +func TestParseConstraintError(t *testing.T) { + expr := "linux,386 my-os,!cgo" + _, err := ParseConstraint(expr) + if err == nil { + t.Fatalf("expected error parsing %q", expr) + } +}