feat: check build constraints in filenames and comments (#144)

This commit is contained in:
Marc Vertes
2019-04-02 15:51:44 +02:00
committed by Ludovic Fernandez
parent d055747bef
commit 10a8312d2c
6 changed files with 250 additions and 10 deletions

13
_test/build0.go Normal file
View File

@@ -0,0 +1,13 @@
// A test program
// +build darwin,linux !arm
// +build go1.12 !go1.13
package main
func main() {
println("hello world")
}
// Output:
// hello world

View File

@@ -310,17 +310,17 @@ func (interp *Interpreter) firstToken(src string) token.Token {
return tok
}
// Note: no type analysis is performed at this stage, it is done in pre-order processing
// of CFG, in order to accommodate forward type declarations
// Note: no type analysis is performed at this stage, it is done in pre-order
// processing of CFG, in order to accommodate forward type declarations
// ast parses src string containing Go code and generates the corresponding AST.
// The package name and the AST root node are returned.
func (interp *Interpreter) ast(src, name string) (string, *Node, error) {
var inFunc bool
// Allow incremental parsing of declarations or statements, by inserting them in a pseudo
// file package or function.
// Those statements or declarations will be always evaluated in the global scope
// Allow incremental parsing of declarations or statements, by inserting
// them in a pseudo file package or function. Those statements or
// declarations will be always evaluated in the global scope
switch interp.firstToken(src) {
case token.PACKAGE:
// nothing to do
@@ -331,6 +331,10 @@ func (interp *Interpreter) ast(src, name string) (string, *Node, error) {
src = "package _; func _() {" + src + "}"
}
if !interp.buildOk(name, src) {
return "", nil, nil // skip source not matching build constraints
}
f, err := parser.ParseFile(interp.fset, name, src, 0)
if err != nil {
return "", nil, err

149
interp/build.go Normal file
View File

@@ -0,0 +1,149 @@
package interp
import (
"go/parser"
"path"
"runtime"
"strconv"
"strings"
)
// buildOk returns true if a file or script matches build constraints
// as specified in https://golang.org/pkg/go/build/#hdr-Build_Constraints
func (interp *Interpreter) buildOk(name, src string) bool {
// Extract comments before the first clause
f, err := parser.ParseFile(interp.fset, name, src, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
return false
}
for _, g := range f.Comments {
// in file, evaluate the AND of multiple line build constraints
for _, line := range strings.Split(strings.TrimSpace(g.Text()), "\n") {
if !buildLineOk(line) {
return false
}
}
}
return true
}
// buildLineOk returns true if line is not a build constraint or
// if build constraint is satisfied
func buildLineOk(line string) (ok bool) {
if len(line) < 7 || line[:7] != "+build " {
return true
}
// In line, evaluate the OR of space-separated options
options := strings.Split(strings.TrimSpace(line[6:]), " ")
for _, o := range options {
if ok = buildOptionOk(o); ok {
break
}
}
return ok
}
// buildOptionOk return true if all comma separated tags match, false otherwise
func buildOptionOk(tag string) bool {
// in option, evaluate the AND of individual tags
for _, t := range strings.Split(tag, ",") {
if !buildTagOk(t) {
return false
}
}
return true
}
var (
goos = runtime.GOOS
goarch = runtime.GOARCH
goversion = goNumVersion()
)
// buildTagOk returns true if a build tag matches, false otherwise
// if first character is !, result is negated
func buildTagOk(s string) (r bool) {
not := s[0] == '!'
if not {
s = s[1:]
}
switch {
case s == goos:
r = true
case s == goarch:
r = true
case len(s) > 4 && s[:4] == "go1.":
if n, err := strconv.Atoi(s[4:]); err != nil {
r = false
} else {
r = goversion >= n
}
}
if not {
r = !r
}
return
}
// goNumVersion returns the go minor version number
func goNumVersion() int {
v := strings.Split(runtime.Version(), ".")
n, _ := strconv.Atoi(v[1])
return n
}
// skipFile returns true if file should be skipped
func skipFile(p string) bool {
if !strings.HasSuffix(p, ".go") {
return true
}
p = strings.TrimSuffix(path.Base(p), ".go")
if strings.HasSuffix(p, "_test") {
return true
}
i := strings.Index(p, "_")
if i < 0 {
return false
}
a := strings.Split(p[i+1:], "_")
last := len(a) - 1
if last1 := last - 1; last1 >= 0 && a[last1] == goos && a[last] == goarch {
return false
}
if s := a[last]; s != goos && s != goarch && knownOs[s] || knownArch[s] {
return true
}
return false
}
var knownOs = map[string]bool{
"aix": true,
"android": true,
"darwin": true,
"dragonfly": true,
"freebsd": true,
"js": true,
"linux": true,
"nacl": true,
"netbsd": true,
"openbsd": true,
"plan9": true,
"solaris": true,
"windows": true,
}
var knownArch = map[string]bool{
"386": true,
"amd64": true,
"amd64p32": true,
"arm": true,
"arm64": true,
"mips": true,
"mips64": true,
"mips64le": true,
"mipsle": true,
"ppc64": true,
"ppc64le": true,
"s390x": true,
"wasm": true,
}

74
interp/build_test.go Normal file
View File

@@ -0,0 +1,74 @@
package interp
import (
"testing"
)
type testBuild struct {
src string
res bool
}
func TestBuildTag(t *testing.T) {
// Assume a specific OS, arch and go version no matter the real underlying system
oo, oa, ov := goos, goarch, goversion
goos, goarch, goversion = "linux", "amd64", 11
defer func() { goos, goarch, goversion = oo, oa, ov }()
tests := []testBuild{
{"// +build linux", true},
{"// +build windows", false},
{"// +build go1.11", true},
{"// +build !go1.12", true},
{"// +build go1.12", false},
{"// +build !go1.10", false},
{"// +build go1.9", true},
{"// +build ignore", false},
{"// +build linux,amd64", true},
{"// +build linux,i386", false},
{"// +build linux,i386 go1.11", true},
{"// +build linux\n// +build amd64", true},
{"// +build linux\n\n\n// +build amd64", true},
{"// +build linux\n// +build i386", false},
}
i := New(Opt{})
for _, test := range tests {
test := test
src := test.src + "\npackage x"
t.Run("", func(t *testing.T) {
if r := i.buildOk("", src); r != test.res {
t.Errorf("got %v, want %v", r, test.res)
}
})
}
}
func TestBuildFile(t *testing.T) {
// Assume a specific OS, arch and go pattern no matter the real underlying system
oo, oa := goos, goarch
goos, goarch = "linux", "amd64"
defer func() { goos, goarch = oo, oa }()
tests := []testBuild{
{"foo/bar_linux_amd64.go", false},
{"foo/bar.go", false},
{"bar.go", false},
{"bar_linux.go", false},
{"bar_maix.go", false},
{"bar_mlinux.go", false},
{"bar_aix_foo.go", false},
{"bar_aix_s390x.go", true},
{"bar_aix_amd64.go", true},
{"bar_linux_arm.go", true},
}
for _, test := range tests {
test := test
t.Run(test.src, func(t *testing.T) {
if r := skipFile(test.src); r != test.res {
t.Errorf("got %v, want %v", r, test.res)
}
})
}
}

View File

@@ -194,7 +194,7 @@ func (i *Interpreter) Eval(src string) (reflect.Value, error) {
// Parse source to AST
pkgName, root, err := i.ast(src, i.Name)
if err != nil {
if err != nil || root == nil {
return res, err
}

View File

@@ -28,10 +28,7 @@ func (i *Interpreter) importSrcFile(rPath, path, alias string) error {
// Parse source files
for _, file := range files {
name := file.Name()
if len(name) <= 3 || name[len(name)-3:] != ".go" {
continue
}
if len(name) > 8 && name[len(name)-8:] == "_test.go" {
if skipFile(name) {
continue
}
@@ -45,6 +42,9 @@ func (i *Interpreter) importSrcFile(rPath, path, alias string) error {
if pname, root, err = i.ast(string(buf), name); err != nil {
return err
}
if root == nil {
continue
}
if pkgName == "" {
pkgName = pname
} else if pkgName != pname {