Skip to content

Commit

Permalink
fix: indexing of outside variables must be evaluated. (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
i4ki committed Jun 9, 2022
1 parent 2d30df2 commit 7604e4a
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 31 deletions.
124 changes: 124 additions & 0 deletions generate/genhcl/genhcl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2092,6 +2092,130 @@ func TestPartialEval(t *testing.T) {
str("string", "b"),
),
},
{
name: "indexing of outside variables",
globals: hcldoc(
globals(
number("depth", 1),
),
),
config: hcldoc(
expr("folder_id", `data.google_active_folder[global.depth].0.id`),
),
want: hcldoc(
expr("folder_id", `data.google_active_folder[1].0.id`),
),
},
{
name: "indexing of outside variables with interpolation of single var",
globals: hcldoc(
globals(
number("depth", 1),
),
),
config: hcldoc(
expr("folder_id", `data.google_active_folder["${global.depth}"].0.id`),
),
want: hcldoc(
expr("folder_id", `data.google_active_folder[1].0.id`),
),
},
{
name: "indexing of outside variables with interpolation",
globals: hcldoc(
globals(
number("depth", 1),
),
),
config: hcldoc(
expr("folder_id", `data.google_active_folder["l${global.depth}"].0.id`),
),
want: hcldoc(
expr("folder_id", `data.google_active_folder["l1"].0.id`),
),
},
{
name: "outside variable with splat operator",
config: hcldoc(
expr("folder_id", `data.test[*].0.id`),
),
want: hcldoc(
expr("folder_id", `data.test[*].0.id`),
),
},
{
name: "outside variable with splat getattr operator",
config: hcldoc(
expr("folder_id", `data.test.*.0.id`),
),
want: hcldoc(
expr("folder_id", `data.test.*.0.id`),
),
},
{
name: "multiple indexing",
config: hcldoc(
expr("a", `data.test[0][0][0]`),
),
want: hcldoc(
expr("a", `data.test[0][0][0]`),
),
},
{
name: "multiple indexing with evaluation",
globals: hcldoc(
globals(
number("val", 1),
),
),
config: hcldoc(
expr("a", `data.test[global.val][0][0]`),
),
want: hcldoc(
expr("a", `data.test[1][0][0]`),
),
},
{
name: "multiple indexing with evaluation 2",
globals: hcldoc(
globals(
number("val", 1),
),
),
config: hcldoc(
expr("a", `data.test[0][global.val][global.val+1]`),
),
want: hcldoc(
expr("a", `data.test[0][1][1+1]`),
),
},
{
name: "nested indexing",
globals: hcldoc(
globals(
expr("obj", `{
key = {
key2 = {
val = "hello"
}
}
}`),
expr("obj2", `{
keyname = "key"
}`),
expr("key", `{
key2 = "keyname"
}`),
str("key2", "key2"),
),
),
config: hcldoc(
expr("hello", `global.obj[global.obj2[global.key[global.key2]]][global.key2]["val"]`),
),
want: hcldoc(
str("hello", "hello"),
),
},
{
name: "obj for loop without eval references",
config: hcldoc(
Expand Down
94 changes: 63 additions & 31 deletions hcl/eval/partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,36 @@ func (e *engine) emitn(n int) {
}
}

func (e *engine) emitVariable(v variable) {
func (e *engine) emitVariable(v variable) error {
tos := e.evalstack.peek()
for i, original := range v.index {
// Prepare a subengine to evaluate the indexing tokens.
// The partial eval engine expects the token stream to be EOF terminated.
index := copytokens(original)
index = append(index, tokenEOF())
subengine := newPartialEvalEngine(index, e.ctx)
subengine.multiline++

// this will only fail in the case of `<ident>[]` but this would be a
// syntax error (hopeful) caught by hcl lib.
tok := subengine.peekn(subengine.skip(0))
if tok.Type != hclsyntax.TokenStar {
evaluatedIndex, err := subengine.Eval()
if err != nil {
return err
}

// remove EOF
v.index[i] = evaluatedIndex[0 : len(evaluatedIndex)-1]
}
}
tos.pushEvaluated(v.alltokens()...)
for i := 0; i < v.size(); i++ {
tos.pushOriginal(e.peek())
for _, tok := range v.original {
tos.pushOriginal(tok)
e.pos++
}

return nil
}

func (e *engine) emitTokens(original hclwrite.Tokens, evaluated hclwrite.Tokens) {
Expand Down Expand Up @@ -252,7 +275,7 @@ func (e *engine) emitnlparens() {

func (e *engine) skipTokens(from int, tokens ...hclsyntax.TokenType) int {
i := from
for e.hasTokens() {
for i < len(e.tokens) {
found := false
for _, t := range tokens {
if e.peekn(i).Type == t {
Expand Down Expand Up @@ -397,13 +420,13 @@ loop:
e.peek().Type, e.tokens[e.pos:].Bytes()))
}

e.emitnlparens()
e.emitComments()

if !e.hasTokens() {
return nil
}

e.emitnlparens()
e.emitComments()

// exprTerm INDEX,GETATTR,SPLAT (expression acessors)
tok = e.peek()
switch tok.Type {
Expand All @@ -418,6 +441,10 @@ loop:
e.emitnlparens()
e.emitComments()

if !e.hasTokens() {
return nil
}

// operation && conditional

tok = e.peek()
Expand Down Expand Up @@ -486,7 +513,7 @@ func (e *engine) evalAcessors() error {
parsed = true
}

if e.peek().Type == hclsyntax.TokenDot {
if e.hasTokens() && e.peek().Type == hclsyntax.TokenDot {
err := e.evalGetAttr()
if err != nil {
return err
Expand All @@ -511,6 +538,8 @@ func (e *engine) evalIndex() error {
e.newnode()
e.multiline++

defer func() { e.multiline-- }()

e.assert(hclsyntax.TokenOBrack)
e.emit()

Expand All @@ -535,6 +564,11 @@ func (e *engine) evalIndex() error {
e.emit()
e.emitnlparens()
e.emitComments()

if !e.hasTokens() {
return nil
}

tok := e.peek()
switch tok.Type {
case hclsyntax.TokenOBrack, hclsyntax.TokenDot:
Expand All @@ -545,8 +579,6 @@ func (e *engine) evalIndex() error {
e.commit()
}

e.multiline--

return nil
}

Expand Down Expand Up @@ -721,7 +753,10 @@ func (e *engine) evalForExpr(matchOpenType, matchCloseType hclsyntax.TokenType)
)
}

e.emitVariable(v)
err := e.emitVariable(v)
if err != nil {
return err
}
} else {
e.emit()
}
Expand Down Expand Up @@ -841,8 +876,7 @@ func (e *engine) evalVar() error {
}

if !v.isTerramate {
e.emitVariable(v)
return nil
return e.emitVariable(v)
}

var expr []byte
Expand Down Expand Up @@ -1138,14 +1172,17 @@ func (e *engine) parseVariable(tokens hclwrite.Tokens) (v variable, found bool)
nsvar := string(v.name[0].Bytes)
v.isTerramate = nsvar == "global" || nsvar == "terramate"

if pos < len(tokens) && tokens[pos].Type == hclsyntax.TokenOBrack {
v.index = parseIndexing(tokens[pos:])
for pos < len(tokens) && tokens[pos].Type == hclsyntax.TokenOBrack {
index, skip := parseIndexing(tokens[pos:])
v.index = append(v.index, index)
pos += skip
}

v.original = tokens[0:pos]
return v, true
}

func parseIndexing(tokens hclwrite.Tokens) hclwrite.Tokens {
func parseIndexing(tokens hclwrite.Tokens) (hclwrite.Tokens, int) {
assertToken(tokens[0], hclsyntax.TokenOBrack)

pos := 1
Expand All @@ -1165,22 +1202,14 @@ func parseIndexing(tokens hclwrite.Tokens) hclwrite.Tokens {
}

if matchingBracks == 0 {
if tokens[pos+1].Type == hclsyntax.TokenOBrack {
// beginning of next '[' sequence.
// this is for the parsing of a.b[<expr][<expr2]...
matchingBracks++
pos += 2
continue
}

break
}

pos++
}

assertToken(tokens[pos], hclsyntax.TokenCBrack)
return tokens[1:pos]
return tokens[1:pos], pos + 1
}

func (e *engine) canEvaluateIdent() bool {
Expand Down Expand Up @@ -1367,26 +1396,29 @@ func copytoken(tok *hclwrite.Token) *hclwrite.Token {

// variable is a low-level representation of a variable in terms of tokens.
type variable struct {
name hclwrite.Tokens
index hclwrite.Tokens
name hclwrite.Tokens

// a variable can have nested indexing. eg.: global.a[0][1][global.b][0]
index []hclwrite.Tokens

original hclwrite.Tokens
isTerramate bool
}

func (v variable) alltokens() hclwrite.Tokens {
tokens := v.name
if len(v.index) > 0 {
for _, index := range v.index {
tokens = append(tokens, tokenOBrack())
tokens = append(tokens, v.index...)
tokens = append(tokens, index...)
tokens = append(tokens, tokenCBrack())
}
return tokens
}

func (v variable) size() int {
sz := len(v.name)
if len(v.index) > 0 {
sz += len(v.index) + 2 // `[` <tokens> `]`
for _, index := range v.index {
sz += len(index) + 2 // `[` <tokens> `]`
}
return sz
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A0.0[*]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A.0[0.*]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A.0[0\n]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A.0[0[0]]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("\"${(\"${(A.0[\n*])}\")}\"")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A.0[A0\n]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A00.0[0][0]")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("A.0[0.A]")
6 changes: 6 additions & 0 deletions hcl/eval/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ func tokenCBrack() *hclwrite.Token {
Bytes: []byte("]"),
}
}

func tokenEOF() *hclwrite.Token {
return &hclwrite.Token{
Type: hclsyntax.TokenEOF,
}
}

0 comments on commit 7604e4a

Please sign in to comment.