Skip to content

Commit 175420c

Browse files
authored
all: overcoming srcimporter issues (#288)
See go-critic/go-critic#1126 We're using the host-specific `go env` info to avoid incorrect GOROOT and other Go build context variables.
1 parent 84a8b0b commit 175420c

File tree

9 files changed

+165
-79
lines changed

9 files changed

+165
-79
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ lint:
3030
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH_DIR)/bin v1.30.0
3131
$(GOPATH_DIR)/bin/golangci-lint run ./...
3232
go build -o go-ruleguard ./cmd/ruleguard
33-
./go-ruleguard -rules rules.go ./...
33+
./go-ruleguard -debug-imports -rules rules.go ./...
3434
@echo "everything is OK"
3535

3636
.PHONY: lint test test-master build build-release

analyzer/analyzer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ func prepareEngine() (*ruleguard.Engine, error) {
169169

170170
func newEngine() (*ruleguard.Engine, error) {
171171
e := ruleguard.NewEngine()
172+
e.InferBuildContext()
172173
fset := token.NewFileSet()
173174

174175
disabledGroups := make(map[string]bool)

cmd/ruleguard/main.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,10 @@
11
package main
22

33
import (
4-
"go/build"
5-
"log"
6-
"os/exec"
7-
"strings"
8-
94
"github.com/quasilyte/go-ruleguard/analyzer"
105
"golang.org/x/tools/go/analysis/singlechecker"
116
)
127

138
func main() {
14-
// If we don't do this, release binaries will have GOROOT set
15-
// to the `go env GOROOT` of the machine that built them.
16-
//
17-
// Usually, it doesn't matter, but since we're using "source"
18-
// importers, it *will* use build.Default.GOROOT to locate packages.
19-
//
20-
// Example: release binary was built with GOROOT="/foo/bar/go",
21-
// user has GOROOT at "/usr/local/go"; if we don't adjust GOROOT
22-
// field here, it'll be "/foo/bar/go".
23-
build.Default.GOROOT = hostGOROOT()
24-
259
singlechecker.Main(analyzer.Analyzer)
2610
}
27-
28-
func hostGOROOT() string {
29-
// `go env GOROOT` should return the correct value even
30-
// if it was overwritten by explicit GOROOT env var.
31-
out, err := exec.Command("go", "env", "GOROOT").CombinedOutput()
32-
if err != nil {
33-
log.Fatalf("infer GOROOT: %v: %s", err, out)
34-
}
35-
return strings.TrimSpace(string(out))
36-
}

internal/xsrcimporter/xsrcimporter.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package xsrcimporter
2+
3+
import (
4+
"go/build"
5+
"go/importer"
6+
"go/token"
7+
"go/types"
8+
"unsafe"
9+
)
10+
11+
func New(ctxt *build.Context, fset *token.FileSet) types.Importer {
12+
imp := importer.ForCompiler(fset, "source", nil)
13+
ifaceVal := *(*iface)(unsafe.Pointer(&imp))
14+
srcImp := (*srcImporter)(ifaceVal.data)
15+
srcImp.ctxt = ctxt
16+
return imp
17+
}
18+
19+
type iface struct {
20+
_ *byte
21+
data unsafe.Pointer
22+
}
23+
24+
type srcImporter struct {
25+
ctxt *build.Context
26+
_ *token.FileSet
27+
_ types.Sizes
28+
_ map[string]*types.Package
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package xsrcimporter
2+
3+
import (
4+
"go/build"
5+
"go/importer"
6+
"go/token"
7+
"reflect"
8+
"testing"
9+
"unsafe"
10+
)
11+
12+
func TestSize(t *testing.T) {
13+
fset := token.NewFileSet()
14+
imp := importer.ForCompiler(fset, "source", nil)
15+
have := unsafe.Sizeof(srcImporter{})
16+
want := reflect.ValueOf(imp).Elem().Type().Size()
17+
if have != want {
18+
t.Errorf("sizes mismatch: have %d want %d", have, want)
19+
}
20+
}
21+
22+
func TestImport(t *testing.T) {
23+
fset := token.NewFileSet()
24+
imp := New(&build.Default, fset)
25+
26+
packages := []string{
27+
"errors",
28+
"fmt",
29+
"encoding/json",
30+
}
31+
32+
for _, pkgPath := range packages {
33+
pkg, err := imp.Import(pkgPath)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
if pkg.Path() != pkgPath {
38+
t.Fatalf("%s: pkg path mismatch (got %s)", pkgPath, pkg.Path())
39+
}
40+
if !pkg.Complete() {
41+
t.Fatalf("%s is incomplete", pkgPath)
42+
}
43+
}
44+
45+
}

ruleguard/engine.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package ruleguard
22

33
import (
4+
"bytes"
45
"errors"
56
"fmt"
67
"go/ast"
8+
"go/build"
79
"go/token"
810
"go/types"
911
"io"
1012
"io/ioutil"
13+
"os/exec"
1114
"sort"
15+
"strconv"
1216
"strings"
1317
"sync"
1418

@@ -41,7 +45,7 @@ func (e *engine) LoadedGroups() []GoRuleGroup {
4145
return result
4246
}
4347

44-
func (e *engine) Load(ctx *LoadContext, filename string, r io.Reader) error {
48+
func (e *engine) Load(ctx *LoadContext, buildContext *build.Context, filename string, r io.Reader) error {
4549
data, err := ioutil.ReadAll(r)
4650
if err != nil {
4751
return err
@@ -50,6 +54,7 @@ func (e *engine) Load(ctx *LoadContext, filename string, r io.Reader) error {
5054
fset: ctx.Fset,
5155
debugImports: ctx.DebugImports,
5256
debugPrint: ctx.DebugPrint,
57+
buildContext: buildContext,
5358
})
5459
irfile, pkg, err := convertAST(ctx, imp, filename, data)
5560
if err != nil {
@@ -82,11 +87,12 @@ func (e *engine) Load(ctx *LoadContext, filename string, r io.Reader) error {
8287
return nil
8388
}
8489

85-
func (e *engine) LoadFromIR(ctx *LoadContext, filename string, f *ir.File) error {
90+
func (e *engine) LoadFromIR(ctx *LoadContext, buildContext *build.Context, filename string, f *ir.File) error {
8691
imp := newGoImporter(e.state, goImporterConfig{
8792
fset: ctx.Fset,
8893
debugImports: ctx.DebugImports,
8994
debugPrint: ctx.DebugPrint,
95+
buildContext: buildContext,
9096
})
9197
config := irLoaderConfig{
9298
state: e.state,
@@ -114,12 +120,12 @@ func (e *engine) LoadFromIR(ctx *LoadContext, filename string, f *ir.File) error
114120
return nil
115121
}
116122

117-
func (e *engine) Run(ctx *RunContext, f *ast.File) error {
123+
func (e *engine) Run(ctx *RunContext, buildContext *build.Context, f *ast.File) error {
118124
if e.ruleSet == nil {
119125
return errors.New("used Run() with an empty rule set; forgot to call Load() first?")
120126
}
121127
rset := e.ruleSet
122-
return newRulesRunner(ctx, e.state, rset).run(f)
128+
return newRulesRunner(ctx, buildContext, e.state, rset).run(f)
123129
}
124130

125131
// engineState is a shared state inside the engine.
@@ -231,3 +237,37 @@ func (state *engineState) findTypeNoCache(importer *goImporter, currentPkg *type
231237
state.typeByFQN[fqn] = typ
232238
return typ, nil
233239
}
240+
241+
func inferBuildContext() *build.Context {
242+
goEnv := func() map[string]string {
243+
out, err := exec.Command("go", "env").CombinedOutput()
244+
if err != nil {
245+
return nil
246+
}
247+
vars := make(map[string]string)
248+
for _, l := range bytes.Split(out, []byte("\n")) {
249+
parts := strings.Split(strings.TrimSpace(string(l)), "=")
250+
if len(parts) != 2 {
251+
continue
252+
}
253+
val, err := strconv.Unquote(parts[1])
254+
if err != nil {
255+
continue
256+
}
257+
vars[parts[0]] = val
258+
}
259+
return vars
260+
}
261+
262+
// Inherit most fields from the build.Default.
263+
ctx := build.Default
264+
265+
env := goEnv()
266+
267+
ctx.GOROOT = env["GOROOT"]
268+
ctx.GOPATH = env["GOPATH"]
269+
ctx.GOARCH = env["GOARCH"]
270+
ctx.GOOS = env["GOOS"]
271+
272+
return &ctx
273+
}

ruleguard/importer.go

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ package ruleguard
22

33
import (
44
"fmt"
5-
"go/ast"
5+
"go/build"
66
"go/importer"
7-
"go/parser"
87
"go/token"
98
"go/types"
10-
"path/filepath"
119
"runtime"
1210

13-
"github.com/quasilyte/go-ruleguard/internal/golist"
11+
"github.com/quasilyte/go-ruleguard/internal/xsrcimporter"
1412
)
1513

1614
// goImporter is a `types.Importer` that tries to load a package no matter what.
@@ -23,7 +21,8 @@ type goImporter struct {
2321
defaultImporter types.Importer
2422
srcImporter types.Importer
2523

26-
fset *token.FileSet
24+
fset *token.FileSet
25+
buildContext *build.Context
2726

2827
debugImports bool
2928
debugPrint func(string)
@@ -33,17 +32,20 @@ type goImporterConfig struct {
3332
fset *token.FileSet
3433
debugImports bool
3534
debugPrint func(string)
35+
buildContext *build.Context
3636
}
3737

3838
func newGoImporter(state *engineState, config goImporterConfig) *goImporter {
39-
return &goImporter{
39+
imp := &goImporter{
4040
state: state,
4141
fset: config.fset,
4242
debugImports: config.debugImports,
4343
debugPrint: config.debugPrint,
4444
defaultImporter: importer.Default(),
45-
srcImporter: importer.ForCompiler(config.fset, "source", nil),
45+
buildContext: config.buildContext,
4646
}
47+
imp.initSourceImporter()
48+
return imp
4749
}
4850

4951
func (imp *goImporter) Import(path string) (*types.Package, error) {
@@ -54,63 +56,40 @@ func (imp *goImporter) Import(path string) (*types.Package, error) {
5456
return pkg, nil
5557
}
5658

57-
pkg, err1 := imp.srcImporter.Import(path)
58-
if err1 == nil {
59+
pkg, srcErr := imp.srcImporter.Import(path)
60+
if srcErr == nil {
5961
imp.state.AddCachedPackage(path, pkg)
6062
if imp.debugImports {
6163
imp.debugPrint(fmt.Sprintf(`imported "%s" from source importer`, path))
6264
}
6365
return pkg, nil
6466
}
6567

66-
pkg, err2 := imp.defaultImporter.Import(path)
67-
if err2 == nil {
68+
pkg, defaultErr := imp.defaultImporter.Import(path)
69+
if defaultErr == nil {
6870
imp.state.AddCachedPackage(path, pkg)
6971
if imp.debugImports {
7072
imp.debugPrint(fmt.Sprintf(`imported "%s" from %s importer`, path, runtime.Compiler))
7173
}
7274
return pkg, nil
7375
}
7476

75-
// Fallback to `go list` as a last resort.
76-
pkg, err3 := imp.golistImport(path)
77-
if err3 == nil {
78-
imp.state.AddCachedPackage(path, pkg)
79-
if imp.debugImports {
80-
imp.debugPrint(fmt.Sprintf(`imported "%s" from golist importer`, path))
81-
}
82-
return pkg, nil
83-
}
84-
8577
if imp.debugImports {
8678
imp.debugPrint(fmt.Sprintf(`failed to import "%s":`, path))
87-
imp.debugPrint(fmt.Sprintf(" source importer: %v", err1))
88-
imp.debugPrint(fmt.Sprintf(" %s importer: %v", runtime.Compiler, err2))
89-
imp.debugPrint(fmt.Sprintf(" golist importer: %v", err3))
79+
imp.debugPrint(fmt.Sprintf(" %s importer: %v", runtime.Compiler, defaultErr))
80+
imp.debugPrint(fmt.Sprintf(" source importer: %v", srcErr))
81+
imp.debugPrint(fmt.Sprintf(" GOROOT=%q GOPATH=%q", imp.buildContext.GOROOT, imp.buildContext.GOPATH))
9082
}
9183

92-
return nil, err2
84+
return nil, defaultErr
9385
}
9486

95-
func (imp *goImporter) golistImport(path string) (*types.Package, error) {
96-
golistPkg, err := golist.JSON(path)
97-
if err != nil {
98-
return nil, err
99-
}
100-
101-
files := make([]*ast.File, 0, len(golistPkg.GoFiles))
102-
for _, filename := range golistPkg.GoFiles {
103-
fullname := filepath.Join(golistPkg.Dir, filename)
104-
f, err := parser.ParseFile(imp.fset, fullname, nil, 0)
105-
if err != nil {
106-
return nil, err
87+
func (imp *goImporter) initSourceImporter() {
88+
if imp.buildContext == nil {
89+
if imp.debugImports {
90+
imp.debugPrint("using build.Default context")
10791
}
108-
files = append(files, f)
92+
imp.buildContext = &build.Default
10993
}
110-
111-
// TODO: do we want to assign imp as importer for this nested typecherker?
112-
// Otherwise it won't be able to resolve imports.
113-
var typecheker types.Config
114-
var info types.Info
115-
return typecheker.Check(path, imp.fset, files, &info)
94+
imp.srcImporter = xsrcimporter.New(imp.buildContext, imp.fset)
11695
}

0 commit comments

Comments
 (0)