From 6709d6bcb5db8e61b76ba8c04f6cd28a2e544284 Mon Sep 17 00:00:00 2001 From: Steffen Neubauer Date: Mon, 9 Dec 2024 12:14:07 +0100 Subject: [PATCH] fix: retain bug-compatibility for referencing missing variables in unary operators (`!` and `typeof`). Older versions of garden did not throw an error when referencing missing variables from unary operators like `!` and `typeof`. This commit restores backwards-compatiblity that has been broken in #6685 --- core/src/template-string/ast.ts | 28 +++++++++++++++++++----- core/test/unit/src/template-string.ts | 31 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/core/src/template-string/ast.ts b/core/src/template-string/ast.ts index 10ab68f9fd..0c33ed155f 100644 --- a/core/src/template-string/ast.ts +++ b/core/src/template-string/ast.ts @@ -160,26 +160,44 @@ export abstract class UnaryExpression extends TemplateExpression { } override evaluate(args: EvaluateArgs): TemplateEvaluationResult { - const inner = this.innerExpression.evaluate(args) + const inner = this.innerExpression.evaluate({ + ...args, + // For backwards compatibility with older versions of Garden, unary expressions do not throw errors if context lookup expressions fail. + // `!var.doesNotExist` evaluates to false and `typeof var.doesNotExist` evaluates to the string "undefined". + // TODO(0.14): Remove the following line. other methods exist to make variables optional, for example using the logical or operator. + optional: true, + }) - if (typeof inner === "symbol") { + if (inner === CONTEXT_RESOLVE_KEY_AVAILABLE_LATER) { return inner } return this.transform(inner) } - abstract transform(value: CollectionOrValue): TemplatePrimitive + abstract transform( + value: CollectionOrValue | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND + ): TemplatePrimitive | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND } export class TypeofExpression extends UnaryExpression { - override transform(value: CollectionOrValue): string { + override transform( + value: CollectionOrValue | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND + ): string | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND { + if (isNotFound(value)) { + return "undefined" + } return typeof value } } export class NotExpression extends UnaryExpression { - override transform(value: CollectionOrValue): boolean { + override transform( + value: CollectionOrValue | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND + ): boolean | typeof CONTEXT_RESOLVE_KEY_NOT_FOUND { + if (isNotFound(value)) { + return true + } return !value } } diff --git a/core/test/unit/src/template-string.ts b/core/test/unit/src/template-string.ts index b7af9f9893..72dd746422 100644 --- a/core/test/unit/src/template-string.ts +++ b/core/test/unit/src/template-string.ts @@ -958,6 +958,37 @@ describe("resolveTemplateString", () => { }) }) + it("should throw an error if var lookup fails combined with json encode", () => { + const c = new GenericContext({ var: {} }) + + void expectError(() => resolveTemplateString({ string: "${jsonEncode(var.missing)}", context: c }), { + contains: "Invalid template string (${jsonEncode(var.missing)}): Could not find key missing under var.", + }) + }) + + it("We need to remain bug-compatible with older versions of garden and not throw when variable does not exist with certain operators", () => { + const testCases = { + "${!var.doesNotExist}": true, + "${typeof var.doesNotExist}": "undefined", + "${! (var.doesNotExistOne && var.doesNotExistTwo)}": true, + "${var.doesNotExistOne && var.doesNotExistTwo}": false, + } + + for (const [template, expectation] of Object.entries(testCases)) { + const result = resolveTemplateString({ + string: template, + contextOpts: { + allowPartial: false, + }, + context: new GenericContext({ var: {} }), + }) + expect(result).to.eq( + expectation, + `Template "${template}" did not resolve to expected value ${JSON.stringify(expectation)}` + ) + } + }) + context("allowPartial=true", () => { it("passes through template strings with missing key", () => { const res = resolveTemplateString({