From a38ed8e12c63c44176028892cbfd6e175261c5a0 Mon Sep 17 00:00:00 2001 From: Kush Zingade Date: Thu, 13 Aug 2026 13:38:06 +0800 Subject: [PATCH 1/2] Add opt-in masking of template expressions before parsing Templating languages that wrap YAML, most commonly Helm's Go templates, produce files that are not valid YAML until they are rendered. A control-flow line such as `{{- if .Values.autoscaling.enabled }}` breaks the document structure, and the parser then reports a cascade of syntax errors on every following line. An inline expression composes as a nested flow map, so schema validation reports a type error instead. Add a `yaml.template` setting. When set to `helm`, `{{ ... }}` spans are replaced with inert text of exactly the same length before the text reaches the parser: - A line containing only a template expression becomes a comment, so control flow drops out of the document structure. - An inline expression becomes a plain scalar, so the value parses as a string. Masking is length-preserving, so every offset stays valid and no position translation is needed downstream. It happens in `YamlDocuments.ensureCache`, the single choke point every feature reads through, which is also why the mode is stored on `YamlDocuments` rather than in `ParserOptions`: most callers of `getYamlDocument` pass no options, so keying it off the options would make masking depend on which feature filled the cache first. The formatter bypasses that cache and returns no edits for templated documents, since prettier would rewrite the template syntax. The setting defaults to `none`, so behaviour is unchanged unless it is enabled. There is still no completion, hover, or validation inside `{{ }}`; this only stops template syntax from breaking the rest of the document. Signed-off-by: Kush Zingade --- README.md | 19 +++ .../handlers/settingsHandlers.ts | 2 + src/languageservice/parser/templateMasking.ts | 74 ++++++++++++ src/languageservice/parser/yaml-documents.ts | 27 ++++- src/languageservice/services/yamlFormatter.ts | 9 ++ src/languageservice/yamlLanguageService.ts | 8 ++ src/yamlSettings.ts | 3 + test/templateMasking.test.ts | 112 ++++++++++++++++++ 8 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/languageservice/parser/templateMasking.ts create mode 100644 test/templateMasking.test.ts diff --git a/README.md b/README.md index a0dc8c3be..591de9cf7 100755 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The server supports the following settings supplied by LSP clients: - `yaml.yamlVersion`: Set default YAML spec version (`1.2` or `1.1`). Defaults to `1.2`. - `yaml.maxItemsComputed`: The maximum number of document symbols and folding regions computed (limited for performance reasons). Defaults to `5000`. +- `yaml.template`: Mask templating expressions before parsing, so that templated documents such as Helm charts parse as plain YAML instead of reporting syntax errors on every `{{ ... }}`. Set to `helm` to enable, `none` to disable. Defaults to `none`. See [Templated documents](#templated-documents). - `yaml.format.enable`: Enable/disable the default YAML formatter. Defaults to `true`. - `yaml.format.singleQuote`: Use single quotes instead of double quotes. Defaults to `false`. - `yaml.format.bracketSpacing`: Print spaces between brackets in objects. Defaults to `true`. @@ -293,6 +294,24 @@ When multiple schema sources or schema-disabling mechanisms apply to the same YA 6. `json/schemaAssociations` notification 7. SchemaStore +## Templated documents + +Templating languages that wrap YAML, such as Helm's Go templates, produce files that are not valid YAML until they are rendered. A control-flow line like `{{- if .Values.autoscaling.enabled }}` breaks the document structure, and the parser reports a cascade of syntax errors on every line that follows. + +Setting `yaml.template` to `helm` masks `{{ ... }}` expressions with inert text of exactly the same length before the document is parsed: + +- A line containing only a template expression becomes a comment, so control flow drops out of the document structure. +- An inline expression such as `replicas: {{ .Values.replicaCount }}` becomes a plain scalar, so the value parses as a string. + +Because masking preserves length, all offsets are unchanged, and diagnostics, hover, completion, and symbol ranges continue to point at the real document. Schema validation, duplicate key detection, completion, hover, folding, and document symbols keep working on the untemplated parts of the file. + +The setting is off by default. Its limits are worth knowing before enabling it: + +- There is no completion, hover, or validation inside `{{ }}`. Masking only stops template syntax from breaking the surrounding document. +- A fully templated value validates as a string, so a schema expecting a number or boolean at that position still reports a type error. +- Expressions that expand to block content, such as `{{ include "chart.labels" . | nindent 4 }}`, and `if`/`else` branches that each define the same key, can still produce false positives. Masking cannot know what a template expands to. +- Formatting is disabled for documents containing template expressions, since the formatter would rewrite the template syntax. + ## Adding custom tags YAML custom tags extend the language with application-specific syntax. Configure custom tags with the `yaml.customTags` setting. diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 8fd67bbee..598eb45c6 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -164,6 +164,7 @@ export class SettingsHandler { flowSequence: settings.yaml.style?.flowSequence ?? 'allow', }; this.yamlSettings.keyOrdering = settings.yaml.keyOrdering ?? false; + this.yamlSettings.template = settings.yaml.template ?? 'none'; } this.yamlSettings.schemaConfigurationSettings = []; @@ -318,6 +319,7 @@ export class SettingsHandler { flowSequence: this.yamlSettings.style?.flowSequence, yamlVersion: this.yamlSettings.yamlVersion, keyOrdering: this.yamlSettings.keyOrdering, + template: this.yamlSettings.template, hoverSchemaSource: this.yamlSettings.yamlHoverSchemaSource, }; diff --git a/src/languageservice/parser/templateMasking.ts b/src/languageservice/parser/templateMasking.ts new file mode 100644 index 000000000..c5caa0bf8 --- /dev/null +++ b/src/languageservice/parser/templateMasking.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Length-preserving masking of Go-template expressions so that templated + * documents (Helm charts) parse as plain YAML. + * + * Every replacement has exactly the same length as the original span and + * newlines are never touched, so all document offsets survive and no + * position translation is needed anywhere downstream. + * + * Rules: + * - A line whose content is only template expressions (plus whitespace) + * becomes a comment of identical length. Control-flow lines such as + * `{{- if .Values.enabled }}` and `{{- end }}` drop out of the document + * structure entirely. + * - An inline expression after other content (`foo: {{ .Values.name }}`) + * is replaced by an unquoted filler scalar of identical length, so the + * value parses as a plain string. + * - A template span crossing line boundaries is masked on every line it + * covers, each line classified by the rules above. + * - Expressions inside quotes are already valid YAML; masking them keeps + * the value a string of the same length, so the parse result class is + * unchanged and the function stays context-free. + */ + +/** Which template dialect to mask. `none` disables masking entirely. */ +export type TemplateMode = 'none' | 'helm'; + +/** Matches a template span, including across line boundaries; an unclosed + * span is masked to the end of its line. */ +const TEMPLATE_SPAN = /\{\{[\s\S]*?\}\}|\{\{[^\n]*$/gm; + +/** Filler character for inline expressions. Parses as a plain scalar. */ +const INLINE_FILL = 'x'; + +/** Internal sentinel; NUL cannot appear in an LSP document's text. */ +const SENTINEL = '\u0000'; + +/** + * Mask Go-template expressions in `text`, preserving length and line + * structure exactly. Returns the input unchanged when it contains no + * template expression. + */ +export function maskTemplates(text: string): string { + if (!text.includes('{{')) { + return text; + } + + // Pass 1: replace every character of every template span with a + // sentinel, preserving newlines, so pass 2 can classify lines. + const masked = text.replace(TEMPLATE_SPAN, (m) => m.replace(/[^\n]/g, SENTINEL)); + + // Pass 2: per line, decide comment-out vs inline filler. + const lines = masked.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (!line.includes(SENTINEL)) { + continue; + } + if (line.split(SENTINEL).join('').trim().length === 0) { + // Template-only line: comment it out at the first non-space column, + // preserving both indentation and total length. + const indent = line.match(/^[ \t]*/)[0].length; + lines[i] = line.slice(0, indent) + '#' + ' '.repeat(Math.max(0, line.length - indent - 1)); + } else { + // Inline expression: same-length filler scalar. + lines[i] = line.split(SENTINEL).join(INLINE_FILL); + } + } + return lines.join('\n'); +} diff --git a/src/languageservice/parser/yaml-documents.ts b/src/languageservice/parser/yaml-documents.ts index f6c108eb5..5d4e814f1 100644 --- a/src/languageservice/parser/yaml-documents.ts +++ b/src/languageservice/parser/yaml-documents.ts @@ -17,6 +17,8 @@ import { isArrayEqual } from '../utils/arrUtils'; import { getParent } from '../utils/yamlAstUtils'; import type { TextBuffer } from '../utils/textBuffer'; import { getIndentation } from '../utils/strings'; +import type { TemplateMode } from './templateMasking'; +import { maskTemplates } from './templateMasking'; /** * These documents are collected into a final YAMLDocument @@ -266,11 +268,23 @@ export class YAMLDocument { interface YamlCachedDocument { version: number; parserOptions: ParserOptions; + templateMode: TemplateMode; document: YAMLDocument; } export class YamlDocuments { // a mapping of URIs to cached documents private cache = new Map(); + private templateMode: TemplateMode = 'none'; + + /** + * The template mode is global rather than a `ParserOptions` field because + * most callers of `getYamlDocument` pass no options at all; keying it off + * the options would make masking depend on which feature filled the cache + * first. + */ + configure(settings: { template?: TemplateMode }): void { + this.templateMode = settings?.template ?? 'none'; + } /** * Get cached YAMLDocument @@ -294,14 +308,24 @@ export class YamlDocuments { private ensureCache(document: TextDocument, parserOptions: ParserOptions, addRootObject: boolean): void { const key = document.uri; if (!this.cache.has(key)) { - this.cache.set(key, { version: -1, document: new YAMLDocument([], []), parserOptions: defaultOptions }); + this.cache.set(key, { + version: -1, + document: new YAMLDocument([], []), + parserOptions: defaultOptions, + templateMode: this.templateMode, + }); } const cacheEntry = this.cache.get(key); if ( cacheEntry.version !== document.version || + cacheEntry.templateMode !== this.templateMode || (parserOptions.customTags && !isArrayEqual(cacheEntry.parserOptions.customTags, parserOptions.customTags)) ) { let text = document.getText(); + // Masking is length-preserving, so every offset below stays valid. + if (this.templateMode === 'helm') { + text = maskTemplates(text); + } // if text is contains only whitespace wrap all text in object to force schema selection if (addRootObject && !/\S/.test(text)) { text = `{${text}}`; @@ -310,6 +334,7 @@ export class YamlDocuments { cacheEntry.document = doc; cacheEntry.version = document.version; cacheEntry.parserOptions = parserOptions; + cacheEntry.templateMode = this.templateMode; } } } diff --git a/src/languageservice/services/yamlFormatter.ts b/src/languageservice/services/yamlFormatter.ts index f980d98f4..cbca33488 100644 --- a/src/languageservice/services/yamlFormatter.ts +++ b/src/languageservice/services/yamlFormatter.ts @@ -12,13 +12,16 @@ import * as yamlPlugin from 'prettier/plugins/yaml'; import * as estreePlugin from 'prettier/plugins/estree'; import { format } from 'prettier/standalone'; import type { TextDocument } from 'vscode-languageserver-textdocument'; +import type { TemplateMode } from '../parser/templateMasking'; export class YAMLFormatter { private formatterEnabled = true; + private templateMode: TemplateMode = 'none'; public configure(shouldFormat: LanguageSettings): void { if (shouldFormat) { this.formatterEnabled = shouldFormat.format; + this.templateMode = shouldFormat.template ?? 'none'; } } @@ -33,6 +36,12 @@ export class YAMLFormatter { try { const text = document.getText(); + // Prettier has no notion of template expressions and would rewrite or + // reject them, so a templated document is left untouched. + if (this.templateMode !== 'none' && text.includes('{{')) { + return []; + } + const prettierOptions: Options = { parser: 'yaml', plugins: [yamlPlugin, estreePlugin], diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index 56bfab7af..8922b6540 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -47,6 +47,7 @@ import { doDocumentOnTypeFormatting } from './services/yamlOnTypeFormatting'; import { YamlCodeLens } from './services/yamlCodeLens'; import type { Telemetry } from './telemetry'; import type { YamlVersion } from './parser/yamlParser07'; +import type { TemplateMode } from './parser/templateMasking'; import { YamlCompletion } from './services/yamlCompletion'; import { yamlDocumentsCache } from './parser/yaml-documents'; import type { SettingsState } from '../yamlSettings'; @@ -125,6 +126,12 @@ export interface LanguageSettings { * Show schema source URI in hover popups. Default is true. */ hoverSchemaSource?: boolean; + + /** + * Mask templating expressions before parsing, so that templated documents + * parse as plain YAML. Default is `none`. + */ + template?: TemplateMode; } export interface WorkspaceContextService { @@ -230,6 +237,7 @@ export function getLanguageService(params: { ); }); } + yamlDocumentsCache.configure(settings); yamlValidation.configure(settings); hover.configure(settings); completer.configure(settings, params.yamlSettings); diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index 912878a0e..a5f993106 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -7,6 +7,7 @@ import type { JSONSchema } from './languageservice/jsonSchema'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { CRD_CATALOG_URL, JSON_SCHEMASTORE_URL } from './languageservice/utils/schemaUrls'; import type { YamlVersion } from './languageservice/parser/yamlParser07'; +import type { TemplateMode } from './languageservice/parser/templateMasking'; // Client settings interface to grab settings relevant for the language server export interface Settings { @@ -37,6 +38,7 @@ export interface Settings { flowSequence: 'allow' | 'forbid'; }; keyOrdering: boolean; + template: TemplateMode; maxItemsComputed: number; yamlVersion: YamlVersion; hoverSchemaSource: boolean; @@ -104,6 +106,7 @@ export class SettingsState { flowSequence: 'allow' | 'forbid'; }; keyOrdering = false; + template: TemplateMode = 'none'; maxItemsComputed = 5000; // File validation helpers diff --git a/test/templateMasking.test.ts b/test/templateMasking.test.ts new file mode 100644 index 000000000..47a046cde --- /dev/null +++ b/test/templateMasking.test.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { maskTemplates } from '../src/languageservice/parser/templateMasking'; +import { yamlDocumentsCache } from '../src/languageservice/parser/yaml-documents'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import assert from 'assert'; + +const HELM_CHART = [ + 'apiVersion: apps/v1', + 'kind: Deployment', + 'metadata:', + ' name: {{ include "mychart.fullname" . }}', + ' labels:', + ' {{- include "mychart.labels" . | nindent 4 }}', + 'spec:', + ' {{- if not .Values.autoscaling.enabled }}', + ' replicas: {{ .Values.replicaCount }}', + ' {{- end }}', + ' selector:', + ' matchLabels:', + ' app: {{ .Chart.Name }}', + '', +].join('\n'); + +function errorCount(text: string, template: 'none' | 'helm'): number { + yamlDocumentsCache.clear(); + yamlDocumentsCache.configure({ template }); + const doc = TextDocument.create('file://foo/bar.yaml', 'yaml', 1, text); + return yamlDocumentsCache.getYamlDocument(doc).documents.reduce((n, d) => n + d.errors.length, 0); +} + +describe('Template masking', () => { + afterEach(() => { + yamlDocumentsCache.clear(); + yamlDocumentsCache.configure({ template: 'none' }); + }); + + it('should leave text without templates untouched', () => { + const text = 'foo: bar\nbaz:\n - 1\n'; + assert.strictEqual(maskTemplates(text), text); + }); + + it('should preserve length and line count', () => { + const masked = maskTemplates(HELM_CHART); + assert.strictEqual(masked.length, HELM_CHART.length); + assert.strictEqual(masked.split('\n').length, HELM_CHART.split('\n').length); + }); + + it('should comment out a template-only line, keeping indentation', () => { + const template = ' {{- if .Values.enabled }}'; + const masked = maskTemplates(`spec:\n${template}\n foo: bar`); + const middle = masked.split('\n')[1]; + assert.strictEqual(middle, ' #' + ' '.repeat(template.length - 3)); + assert.strictEqual(middle.length, template.length); + }); + + it('should replace an inline expression with a filler scalar', () => { + const masked = maskTemplates('name: {{ .Values.name }}'); + assert.strictEqual(masked, 'name: xxxxxxxxxxxxxxxxxx'); + }); + + it('should mask a span crossing line boundaries', () => { + const text = 'a: {{ multi\nline }}\nb: plain'; + const masked = maskTemplates(text); + assert.strictEqual(masked.length, text.length); + assert.strictEqual(masked, 'a: xxxxxxxx\n# \nb: plain'); + }); + + it('should mask an unclosed span to the end of its line', () => { + const text = 'a: {{ unclosed\nb: plain'; + const masked = maskTemplates(text); + assert.strictEqual(masked.length, text.length); + assert.strictEqual(masked, 'a: xxxxxxxxxxx\nb: plain'); + }); + + it('should not affect a quoted expression beyond keeping it a string', () => { + const masked = maskTemplates('name: "{{ .Values.name }}"'); + assert.strictEqual(masked, 'name: "xxxxxxxxxxxxxxxxxx"'); + }); + + it('should produce parse errors on a Helm chart when disabled', () => { + assert.ok(errorCount(HELM_CHART, 'none') > 0); + }); + + it('should produce no parse errors on a Helm chart when enabled', () => { + assert.strictEqual(errorCount(HELM_CHART, 'helm'), 0); + }); + + it('should keep offsets valid so nodes map back to the real document', () => { + yamlDocumentsCache.clear(); + yamlDocumentsCache.configure({ template: 'helm' }); + const doc = TextDocument.create('file://foo/bar.yaml', 'yaml', 1, HELM_CHART); + const root = yamlDocumentsCache.getYamlDocument(doc).documents[0].root; + const kind = root.children.find((c) => c.children?.[0]?.value === 'kind'); + assert.strictEqual(HELM_CHART.substr(kind.offset, kind.length), 'kind: Deployment'); + }); + + it('should re-parse when the template mode changes on an unchanged document', () => { + const doc = TextDocument.create('file://foo/bar.yaml', 'yaml', 1, HELM_CHART); + yamlDocumentsCache.clear(); + yamlDocumentsCache.configure({ template: 'none' }); + assert.ok(yamlDocumentsCache.getYamlDocument(doc).documents.reduce((n, d) => n + d.errors.length, 0) > 0); + yamlDocumentsCache.configure({ template: 'helm' }); + assert.strictEqual( + yamlDocumentsCache.getYamlDocument(doc).documents.reduce((n, d) => n + d.errors.length, 0), + 0 + ); + }); +}); From 4508ed8b355188139fc33876ae20064c10d32016 Mon Sep 17 00:00:00 2001 From: Kush Zingade Date: Sat, 15 Aug 2026 01:14:51 +0800 Subject: [PATCH 2/2] Comment out lines wrapped in inline control-flow expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A line like '{{ if eq .Values.favorite.drink "coffee" }}mug: "true"{{ end }}' previously took the inline-filler path, producing trailing filler after a closed quote — a parse error. The wrapped content is conditional and cannot be validated as always-present, so the whole line is commented out, same as standalone control-flow lines. Pass 1 now marks control-flow spans with a distinct sentinel so pass 2 can classify them. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WpgboEXhCRG5bScRWM1YjQ --- src/languageservice/parser/templateMasking.ts | 35 +++++++++++++++---- test/templateMasking.test.ts | 29 +++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/languageservice/parser/templateMasking.ts b/src/languageservice/parser/templateMasking.ts index c5caa0bf8..d9340ccee 100644 --- a/src/languageservice/parser/templateMasking.ts +++ b/src/languageservice/parser/templateMasking.ts @@ -16,6 +16,10 @@ * becomes a comment of identical length. Control-flow lines such as * `{{- if .Values.enabled }}` and `{{- end }}` drop out of the document * structure entirely. + * - A line that starts with a control-flow expression but carries other + * content (`{{ if ... }}mug: "true"{{ end }}`) is also commented out: + * the content is conditional, so it cannot be validated as + * always-present, and relocating it would break offset preservation. * - An inline expression after other content (`foo: {{ .Values.name }}`) * is replaced by an unquoted filler scalar of identical length, so the * value parses as a plain string. @@ -36,8 +40,19 @@ const TEMPLATE_SPAN = /\{\{[\s\S]*?\}\}|\{\{[^\n]*$/gm; /** Filler character for inline expressions. Parses as a plain scalar. */ const INLINE_FILL = 'x'; -/** Internal sentinel; NUL cannot appear in an LSP document's text. */ +/** Internal sentinels; these control characters cannot appear in an LSP + * document's text. The control-flow sentinel marks spans like `{{ if }}` + * so pass 2 can tell them apart from value expressions. */ const SENTINEL = '\u0000'; +const CONTROL_SENTINEL = '\u0001'; +// eslint-disable-next-line no-control-regex +const ANY_SENTINEL = /[\u0000\u0001]/; +// eslint-disable-next-line no-control-regex +const ANY_SENTINEL_GLOBAL = /[\u0000\u0001]/g; + +/** Matches control-flow expressions (`{{ if }}`, `{{- end }}`, ...) that + * affect document structure rather than producing a value. */ +const CONTROL_FLOW = /^\{\{-?\s*(if|else|end|range|with|define|block|template)\b/; /** * Mask Go-template expressions in `text`, preserving length and line @@ -51,23 +66,29 @@ export function maskTemplates(text: string): string { // Pass 1: replace every character of every template span with a // sentinel, preserving newlines, so pass 2 can classify lines. - const masked = text.replace(TEMPLATE_SPAN, (m) => m.replace(/[^\n]/g, SENTINEL)); + // Control-flow spans get a distinct sentinel. + const masked = text.replace(TEMPLATE_SPAN, (m) => m.replace(/[^\n]/g, CONTROL_FLOW.test(m) ? CONTROL_SENTINEL : SENTINEL)); // Pass 2: per line, decide comment-out vs inline filler. const lines = masked.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; - if (!line.includes(SENTINEL)) { + if (!ANY_SENTINEL.test(line)) { continue; } - if (line.split(SENTINEL).join('').trim().length === 0) { - // Template-only line: comment it out at the first non-space column, - // preserving both indentation and total length. + const templateOnly = line.replace(ANY_SENTINEL_GLOBAL, '').trim().length === 0; + // A line that starts with a control-flow expression but carries other + // content ({{ if ... }}mug: "true"{{ end }}) holds conditional content + // that cannot be validated as always-present; drop the whole line. + const wrappedInControlFlow = line.trimStart().startsWith(CONTROL_SENTINEL); + if (templateOnly || wrappedInControlFlow) { + // Comment out at the first non-space column, preserving both + // indentation and total length. const indent = line.match(/^[ \t]*/)[0].length; lines[i] = line.slice(0, indent) + '#' + ' '.repeat(Math.max(0, line.length - indent - 1)); } else { // Inline expression: same-length filler scalar. - lines[i] = line.split(SENTINEL).join(INLINE_FILL); + lines[i] = line.replace(ANY_SENTINEL_GLOBAL, INLINE_FILL); } } return lines.join('\n'); diff --git a/test/templateMasking.test.ts b/test/templateMasking.test.ts index 47a046cde..fb3d5f6bd 100644 --- a/test/templateMasking.test.ts +++ b/test/templateMasking.test.ts @@ -81,6 +81,35 @@ describe('Template masking', () => { assert.strictEqual(masked, 'name: "xxxxxxxxxxxxxxxxxx"'); }); + it('should comment out a line wrapped in inline control flow', () => { + const line = ' {{ if eq .Values.favorite.drink "coffee" }}mug: "true"{{ end }}'; + const masked = maskTemplates(`data:\n${line}\n food: pie`); + const middle = masked.split('\n')[1]; + assert.strictEqual(middle, ' #' + ' '.repeat(line.length - 3)); + assert.strictEqual(middle.length, line.length); + }); + + it('should keep an inline non-control expression before content as filler', () => { + const masked = maskTemplates('{{ .Values.prefix }}name: bar'); + assert.strictEqual(masked, 'xxxxxxxxxxxxxxxxxxxxname: bar'); + }); + + it('should produce no parse errors on a ConfigMap with inline control flow', () => { + const chart = [ + 'apiVersion: v1', + 'kind: ConfigMap', + 'metadata:', + ' name: {{ .Release.Name }}-configmap', + 'data:', + ' myvalue: "Hello World"', + ' drink: {{ .Values.favorite.drink | default "tea" | quote }}', + ' food: {{ .Values.favorite.food | upper | quote }}', + ' {{ if eq .Values.favorite.drink "coffee" }}mug: "true"{{ end }}', + '', + ].join('\n'); + assert.strictEqual(errorCount(chart, 'helm'), 0); + }); + it('should produce parse errors on a Helm chart when disabled', () => { assert.ok(errorCount(HELM_CHART, 'none') > 0); });