Skip to content

Commit 394cb12

Browse files
authored
feat(tool/imports): import analysis using upstream jsonnet (#84)
* feat(jsonnet): switch back to google/go-jsonnet Due to recent changes to google/go-jsonnet we can drop our dirty patches and use the official compiler again! * feat(jsonnet): directly compute imports Because we are now manually working with the AST, we do not need to use the TraceImporter anymore, as we have direct access to the importing process. * chore: go mod tidy * fix(tool/imports): switch to upstream jsonnet As google/go-jsonnet#327 is merged, we do not need my custom fork anymore. Now fully switches to the upstream jsonnet library * test(tool/imports): unit test Adds a unit test checking whether all imports are correctly caught
1 parent fa6dae1 commit 394cb12

17 files changed

+301
-183
lines changed

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ require (
66
github.com/Masterminds/semver v1.4.2
77
github.com/alecthomas/chroma v0.6.6
88
github.com/fatih/color v1.7.0
9-
github.com/google/go-jsonnet v0.13.0
9+
github.com/google/go-jsonnet v0.14.1-0.20191006203837-42cb19ef24fb
1010
github.com/kr/pretty v0.1.0 // indirect
1111
github.com/pkg/errors v0.8.1
1212
github.com/posener/complete v1.2.1
13-
github.com/sh0rez/go-jsonnet v0.14.2
1413
github.com/spf13/cobra v0.0.5
1514
github.com/spf13/pflag v1.0.3
1615
github.com/spf13/viper v1.3.2

go.sum

+2-4
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
3434
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
3535
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
3636
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
37-
github.com/google/go-jsonnet v0.13.0 h1:Ul0FtJiQl705JIyGKaBZug/W2LBY5p0xwY08Q69eOAg=
38-
github.com/google/go-jsonnet v0.13.0/go.mod h1:gNwctc8xrpXNs749bjRLO58rjIBVrWz+pgsRoOCh5Vs=
37+
github.com/google/go-jsonnet v0.14.1-0.20191006203837-42cb19ef24fb h1:6GyG7yy0z1foZBWyfRa9CZYdcLbSPHR19PW70fJBedI=
38+
github.com/google/go-jsonnet v0.14.1-0.20191006203837-42cb19ef24fb/go.mod h1:zPGC9lj/TbjkBtUACIvYR/ILHrFqKRhxeEA+bLyeMnY=
3939
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
4040
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
4141
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -81,8 +81,6 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue
8181
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
8282
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
8383
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
84-
github.com/sh0rez/go-jsonnet v0.14.2 h1:xt6UJNVUR9blFgVrOrRqYwv5Qncp3xgM18fz1t2WjPQ=
85-
github.com/sh0rez/go-jsonnet v0.14.2/go.mod h1:OC3U+HWq8EVB5nvJDJAWvgVb43HFEXc2VqCUipW9/BE=
8684
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
8785
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
8886
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=

pkg/jsonnet/eval.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package jsonnet
2+
3+
import (
4+
"io/ioutil"
5+
"path/filepath"
6+
7+
jsonnet "github.com/google/go-jsonnet"
8+
"github.com/pkg/errors"
9+
10+
"github.com/grafana/tanka/pkg/jpath"
11+
"github.com/grafana/tanka/pkg/native"
12+
)
13+
14+
// EvaluateFile opens the file, reads it into memory and evaluates it afterwards (`Evaluate()`)
15+
func EvaluateFile(jsonnetFile string) (string, error) {
16+
bytes, err := ioutil.ReadFile(jsonnetFile)
17+
if err != nil {
18+
return "", err
19+
}
20+
21+
jpath, _, _, err := jpath.Resolve(filepath.Dir(jsonnetFile))
22+
if err != nil {
23+
return "", errors.Wrap(err, "resolving jpath")
24+
}
25+
return Evaluate(string(bytes), jpath)
26+
}
27+
28+
// Evaluate renders the given jsonnet into a string
29+
func Evaluate(sonnet string, jpath []string) (string, error) {
30+
importer := jsonnet.FileImporter{
31+
JPaths: jpath,
32+
}
33+
34+
vm := jsonnet.MakeVM()
35+
vm.Importer(&importer)
36+
for _, nf := range native.Funcs() {
37+
vm.NativeFunction(nf)
38+
}
39+
40+
return vm.EvaluateSnippet("main.jsonnet", sonnet)
41+
}

pkg/jsonnet/imports.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package jsonnet
2+
3+
import (
4+
"io/ioutil"
5+
"path/filepath"
6+
7+
jsonnet "github.com/google/go-jsonnet"
8+
"github.com/google/go-jsonnet/ast"
9+
"github.com/google/go-jsonnet/toolutils"
10+
"github.com/pkg/errors"
11+
12+
"github.com/grafana/tanka/pkg/jpath"
13+
"github.com/grafana/tanka/pkg/native"
14+
)
15+
16+
// TransitiveImports returns all recursive imports of a file
17+
func TransitiveImports(filename string) ([]string, error) {
18+
sonnet, err := ioutil.ReadFile(filename)
19+
if err != nil {
20+
return nil, errors.Wrap(err, "opening file")
21+
}
22+
23+
jpath, _, _, err := jpath.Resolve(filepath.Dir(filename))
24+
if err != nil {
25+
return nil, errors.Wrap(err, "resolving JPATH")
26+
}
27+
importer := jsonnet.FileImporter{
28+
JPaths: jpath,
29+
}
30+
31+
vm := jsonnet.MakeVM()
32+
vm.Importer(&importer)
33+
for _, nf := range native.Funcs() {
34+
vm.NativeFunction(nf)
35+
}
36+
37+
node, err := jsonnet.SnippetToAST("main.jsonnet", string(sonnet))
38+
if err != nil {
39+
return nil, errors.Wrap(err, "creating Jsonnet AST")
40+
}
41+
42+
imports := make([]string, 0, 0)
43+
err = importRecursive(&imports, vm, node, "main.jsonnet")
44+
45+
return uniqueStringSlice(imports), err
46+
}
47+
48+
// importRecursive takes a Jsonnet VM and recursively imports the AST. Every
49+
// found import is added to the `list` string slice, which will ultimately
50+
// contain all recursive imports
51+
func importRecursive(list *[]string, vm *jsonnet.VM, node ast.Node, currentPath string) error {
52+
switch node := node.(type) {
53+
// we have an `import`
54+
case *ast.Import:
55+
p := node.File.Value
56+
57+
contents, foundAt, err := vm.ImportAST(currentPath, p)
58+
if err != nil {
59+
return errors.Wrap(err, "importing jsonnet")
60+
}
61+
62+
*list = append(*list, foundAt)
63+
64+
if err := importRecursive(list, vm, contents, foundAt); err != nil {
65+
return err
66+
}
67+
68+
// we have an `importstr`
69+
case *ast.ImportStr:
70+
p := node.File.Value
71+
72+
foundAt, err := vm.ResolveImport(currentPath, p)
73+
if err != nil {
74+
return errors.Wrap(err, "importing string")
75+
}
76+
77+
*list = append(*list, foundAt)
78+
79+
// neither `import` nor `importstr`, probably object or similar: try children
80+
default:
81+
for _, child := range toolutils.Children(node) {
82+
if err := importRecursive(list, vm, child, currentPath); err != nil {
83+
return err
84+
}
85+
}
86+
}
87+
return nil
88+
}
89+
90+
func uniqueStringSlice(s []string) []string {
91+
seen := make(map[string]struct{}, len(s))
92+
j := 0
93+
for _, v := range s {
94+
if _, ok := seen[v]; ok {
95+
continue
96+
}
97+
seen[v] = struct{}{}
98+
s[j] = v
99+
j++
100+
}
101+
return s[:j]
102+
}

pkg/jsonnet/imports_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package jsonnet
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestTransitiveImports checks that TransitiveImports is able to report all
11+
// recursive imports of a file
12+
func TestTransitiveImports(t *testing.T) {
13+
imports, err := TransitiveImports("testdata/main.jsonnet")
14+
require.NoError(t, err)
15+
assert.ElementsMatch(t, []string{
16+
"testdata/trees.jsonnet",
17+
18+
"testdata/trees/apple.jsonnet",
19+
"testdata/trees/cherry.jsonnet",
20+
"testdata/trees/peach.jsonnet",
21+
22+
"testdata/trees/generic.libsonnet",
23+
}, imports)
24+
}

pkg/jsonnet/jsonnet.go

-96
This file was deleted.

pkg/jsonnet/testdata/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Importing testdata
2+
3+
This directory contains some jsonnet files importing each other, to test if the import analysis tooling works correctly.
4+
5+
![](test.svg)

pkg/jsonnet/testdata/jsonnetfile.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

pkg/jsonnet/testdata/main.jsonnet

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
local trees = import 'trees.jsonnet';
2+
3+
// a list of trees
4+
[
5+
trees.apple,
6+
trees.cherry,
7+
trees.peach,
8+
]

0 commit comments

Comments
 (0)