diff --git a/hclsyntax/expression.go b/hclsyntax/expression.go index c4a353c4..81597399 100644 --- a/hclsyntax/expression.go +++ b/hclsyntax/expression.go @@ -2013,3 +2013,27 @@ func (e *AnonSymbolExpr) Range() hcl.Range { func (e *AnonSymbolExpr) StartRange() hcl.Range { return e.SrcRange } + +// ExprSyntaxError is a placeholder for an invalid expression that could not +// be parsed due to syntax errors. +type ExprSyntaxError struct { + Placeholder cty.Value + ParseDiags hcl.Diagnostics + SrcRange hcl.Range +} + +func (e *ExprSyntaxError) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { + return e.Placeholder, e.ParseDiags +} + +func (e *ExprSyntaxError) walkChildNodes(w internalWalkFunc) { + // ExprSyntaxError is a leaf node in the tree +} + +func (e *ExprSyntaxError) Range() hcl.Range { + return e.SrcRange +} + +func (e *ExprSyntaxError) StartRange() hcl.Range { + return e.SrcRange +} diff --git a/hclsyntax/expression_template_test.go b/hclsyntax/expression_template_test.go index c78094f5..31198458 100644 --- a/hclsyntax/expression_template_test.go +++ b/hclsyntax/expression_template_test.go @@ -438,6 +438,28 @@ trim`, } +func TestTemplateExprGracefulValue(t *testing.T) { + // we don't care about diags since we know it's invalid config + expr, _ := ParseTemplate([]byte(`prefix${provider::}`), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) + + got, _ := expr.Value(nil) // this should not panic + + if !got.RawEquals(cty.UnknownVal(cty.String).RefineNotNull()) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, cty.UnknownVal(cty.String).RefineNotNull()) + } +} + +func TestTemplateExprWrappedGracefulValue(t *testing.T) { + // we don't care about diags since we know it's invalid config + expr, _ := ParseTemplate([]byte(`${provider::}`), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) + + got, _ := expr.Value(nil) // this should not panic + + if !got.RawEquals(cty.DynamicVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, cty.DynamicVal) + } +} + func TestTemplateExprIsStringLiteral(t *testing.T) { tests := map[string]bool{ // A simple string value is a string literal diff --git a/hclsyntax/expression_test.go b/hclsyntax/expression_test.go index 4f3f75f7..b18214cd 100644 --- a/hclsyntax/expression_test.go +++ b/hclsyntax/expression_test.go @@ -379,8 +379,8 @@ upper( "double::::upper": stdlib.UpperFunc, }, }, - cty.NilVal, - 1, + cty.DynamicVal, + 2, }, { `missing::("foo")`, // missing name after :: @@ -389,8 +389,8 @@ upper( "missing::": stdlib.UpperFunc, }, }, - cty.NilVal, - 1, + cty.DynamicVal, + 2, }, { `misbehave()`, diff --git a/hclsyntax/expression_vars.go b/hclsyntax/expression_vars.go index ce5a5cb7..6c3e472c 100755 --- a/hclsyntax/expression_vars.go +++ b/hclsyntax/expression_vars.go @@ -3,7 +3,7 @@ package hclsyntax -// Generated by expression_vars_get.go. DO NOT EDIT. +// Generated by expression_vars_gen.go. DO NOT EDIT. // Run 'go generate' on this package to update the set of functions here. import ( @@ -22,6 +22,10 @@ func (e *ConditionalExpr) Variables() []hcl.Traversal { return Variables(e) } +func (e *ExprSyntaxError) Variables() []hcl.Traversal { + return Variables(e) +} + func (e *ForExpr) Variables() []hcl.Traversal { return Variables(e) } diff --git a/hclsyntax/expression_vars_gen.go b/hclsyntax/expression_vars_gen.go index efbc2d82..d0888025 100644 --- a/hclsyntax/expression_vars_gen.go +++ b/hclsyntax/expression_vars_gen.go @@ -92,7 +92,7 @@ const outputPreamble = `// Copyright (c) HashiCorp, Inc. package hclsyntax -// Generated by expression_vars_get.go. DO NOT EDIT. +// Generated by expression_vars_gen.go. DO NOT EDIT. // Run 'go generate' on this package to update the set of functions here. import ( diff --git a/hclsyntax/parser.go b/hclsyntax/parser.go index cd9d63d2..ce96ae35 100644 --- a/hclsyntax/parser.go +++ b/hclsyntax/parser.go @@ -1161,15 +1161,20 @@ func (p *parser) finishParsingFunctionCall(name Token) (Expression, hcl.Diagnost for openTok.Type == TokenDoubleColon { nextName := p.Read() if nextName.Type != TokenIdent { - diags = append(diags, &hcl.Diagnostic{ + diag := hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing function name", Detail: "Function scope resolution symbol :: must be followed by a function name in this scope.", Subject: &nextName.Range, Context: hcl.RangeBetween(name.Range, nextName.Range).Ptr(), - }) + } + diags = append(diags, &diag) p.recoverOver(TokenOParen) - return nil, diags + return &ExprSyntaxError{ + ParseDiags: hcl.Diagnostics{&diag}, + Placeholder: cty.DynamicVal, + SrcRange: hcl.RangeBetween(name.Range, nextName.Range), + }, diags } // Initial versions of HCLv2 didn't support function namespaces, and @@ -1192,15 +1197,21 @@ func (p *parser) finishParsingFunctionCall(name Token) (Expression, hcl.Diagnost } if openTok.Type != TokenOParen { - diags = append(diags, &hcl.Diagnostic{ + diag := hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing open parenthesis", Detail: "Function selector must be followed by an open parenthesis to begin the function call.", Subject: &openTok.Range, Context: hcl.RangeBetween(name.Range, openTok.Range).Ptr(), - }) + } + + diags = append(diags, &diag) p.recoverOver(TokenOParen) - return nil, diags + return &ExprSyntaxError{ + ParseDiags: hcl.Diagnostics{&diag}, + Placeholder: cty.DynamicVal, + SrcRange: hcl.RangeBetween(name.Range, openTok.Range), + }, diags } var args []Expression diff --git a/hclsyntax/parser_test.go b/hclsyntax/parser_test.go index 6e226a60..90a75832 100644 --- a/hclsyntax/parser_test.go +++ b/hclsyntax/parser_test.go @@ -2559,6 +2559,120 @@ block "valid" {} }, }, }, + { + "a = partial::namespaced\n", + 1, + &Body{ + Attributes: Attributes{ + "a": { + Name: "a", + Expr: &ExprSyntaxError{ + Placeholder: cty.DynamicVal, + ParseDiags: hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Missing open parenthesis", + Detail: "Function selector must be followed by an open parenthesis to begin the function call.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + Context: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + }, + }, + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + }, + SrcRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + NameRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + }, + EqualsRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 3, Byte: 2}, + End: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + Blocks: Blocks{}, + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + EndRange: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 24}, + }, + }, + }, + { + "a = partial::\n", + 1, + &Body{ + Attributes: Attributes{ + "a": { + Name: "a", + Expr: &ExprSyntaxError{ + Placeholder: cty.DynamicVal, + ParseDiags: hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Missing function name", + Detail: "Function scope resolution symbol :: must be followed by a function name in this scope.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + Context: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + }, + }, + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + }, + SrcRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + NameRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + }, + EqualsRange: hcl.Range{ + Filename: "", + Start: hcl.Pos{Line: 1, Column: 3, Byte: 2}, + End: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + Blocks: Blocks{}, + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + EndRange: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 14}, + }, + }, + }, } for _, test := range tests {