diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index bf5a76e2..3c65af26 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -17,3 +17,6 @@ ### Bug Fixes ### Breaking changes + +- It is now a syntax error to call a builtin function incorrectly. + [449](https://github.com/pulumi/esc/pull/449) \ No newline at end of file diff --git a/ast/environment.go b/ast/environment.go index 03e7b4d1..9ee598ca 100644 --- a/ast/environment.go +++ b/ast/environment.go @@ -128,6 +128,9 @@ func (d *MapDecl[T]) parse(name string, node syntax.Node) syntax.Diagnostics { var v T vname := name + "." + kvp.Key.Value() + if strings.HasPrefix(kvp.Key.Value(), "fn::") { + diags.Extend(syntax.NodeError(kvp.Key, fmt.Sprintf("builtin function call %q not allowed at the top level", kvp.Key.Value()))) + } vdiags := parseNode(vname, &v, kvp.Value) diags.Extend(vdiags...) diff --git a/ast/expr.go b/ast/expr.go index 4dbe23c1..7eacf4c2 100644 --- a/ast/expr.go +++ b/ast/expr.go @@ -625,14 +625,20 @@ func FromBase64(value Expr) *FromBase64Expr { } func tryParseFunction(node *syntax.ObjectNode) (Expr, syntax.Diagnostics, bool) { + var diags syntax.Diagnostics if node.Len() != 1 { - return nil, nil, false + for i := 0; i < node.Len(); i++ { + if k := node.Index(i).Key.Value(); strings.HasPrefix(k, "fn::") { + diags = append(diags, syntax.NodeError(node, fmt.Sprintf("illegal call to builtin function: %q must be the only key within its containing object", k))) + + } + } + return nil, diags, false } kvp := node.Index(0) var parse func(node *syntax.ObjectNode, name *StringExpr, args Expr) (Expr, syntax.Diagnostics) - var diags syntax.Diagnostics switch kvp.Key.Value() { case "fn::fromJSON": parse = parseFromJSON diff --git a/ast/testdata/parse/invalid-invocation/env.yaml b/ast/testdata/parse/invalid-invocation/env.yaml new file mode 100644 index 00000000..a57d41da --- /dev/null +++ b/ast/testdata/parse/invalid-invocation/env.yaml @@ -0,0 +1,6 @@ +values: + multiple-keys: + fn::rotate::provider: + inputs: {} + state: + ohno: true diff --git a/ast/testdata/parse/invalid-invocation/expected.json b/ast/testdata/parse/invalid-invocation/expected.json new file mode 100644 index 00000000..a46a3f07 --- /dev/null +++ b/ast/testdata/parse/invalid-invocation/expected.json @@ -0,0 +1,78 @@ +{ + "decl": { + "Description": null, + "Imports": null, + "Values": { + "Entries": [ + { + "Key": { + "Value": "multiple-keys" + }, + "Value": { + "Entries": [ + { + "Key": { + "Value": "fn::rotate::provider" + }, + "Value": { + "Entries": [ + { + "Key": { + "Value": "inputs" + }, + "Value": { + "Entries": [] + } + } + ] + } + }, + { + "Key": { + "Value": "state" + }, + "Value": { + "Entries": [ + { + "Key": { + "Value": "ohno" + }, + "Value": { + "Value": true + } + } + ] + } + } + ] + } + } + ] + } + }, + "diags": [ + { + "Severity": 1, + "Summary": "illegal call to builtin function: \"fn::rotate::provider\" must be the only key within its containing object", + "Detail": "", + "Subject": { + "Filename": "invalid-invocation", + "Start": { + "Line": 3, + "Column": 5, + "Byte": 29 + }, + "End": { + "Line": 6, + "Column": 17, + "Byte": 95 + } + }, + "Context": null, + "Expression": null, + "EvalContext": null, + "Extra": null, + "Path": "values[\"multiple-keys\"]" + } + ] +} diff --git a/ast/testdata/parse/invalid-invocation2/env.yaml b/ast/testdata/parse/invalid-invocation2/env.yaml new file mode 100644 index 00000000..dcaeaf33 --- /dev/null +++ b/ast/testdata/parse/invalid-invocation2/env.yaml @@ -0,0 +1,2 @@ +values: + fn::secret: "hello" diff --git a/ast/testdata/parse/invalid-invocation2/expected.json b/ast/testdata/parse/invalid-invocation2/expected.json new file mode 100644 index 00000000..fe24a84c --- /dev/null +++ b/ast/testdata/parse/invalid-invocation2/expected.json @@ -0,0 +1,43 @@ +{ + "decl": { + "Description": null, + "Imports": null, + "Values": { + "Entries": [ + { + "Key": { + "Value": "fn::secret" + }, + "Value": { + "Value": "hello" + } + } + ] + } + }, + "diags": [ + { + "Severity": 1, + "Summary": "builtin function call \"fn::secret\" not allowed at the top level", + "Detail": "", + "Subject": { + "Filename": "invalid-invocation2", + "Start": { + "Line": 2, + "Column": 3, + "Byte": 10 + }, + "End": { + "Line": 2, + "Column": 13, + "Byte": 20 + } + }, + "Context": null, + "Expression": null, + "EvalContext": null, + "Extra": null, + "Path": "values[\"fn::secret\"]" + } + ] +}