From 72365cdb7e7e0216333480c66e6b6d287b860f92 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 11:24:10 +0000 Subject: [PATCH] Escape quoted default values so the generated literal compiles DefaultValueIsQuoted wrapped DefaultValue in quotes without escaping it, so a backslash, quote or newline in the value produced C# that does not compile, or that silently changes the string (\t becoming a tab). The value is now written as an escaped regular string literal. This covers the Field, Parameter, Property and EnumMember templates. Fixes #111 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019StKk4VdpfAZ5cdW5X685e --- CodeBlocker.Test/TemplateTests.cs | 17 ++++++++ CodeBlocker/Templates/TemplateBase.cs | 12 ++--- CodeBlocker/Templates/TemplateRendering.cs | 51 ++++++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index 4b809ef..b517a72 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -37,6 +37,17 @@ public void ParameterQuotesItsDefaultValueWhenAsked() => "string name = \"none\"", Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = "none", DefaultValueIsQuoted = true })); + [TestMethod] + [DataRow(@"C:\temp", @"""C:\\temp""", DisplayName = "Backslash")] + [DataRow("say \"hi\"", @"""say \""hi\""""", DisplayName = "Quote")] + [DataRow("one\ntwo\r\n", @"""one\ntwo\r\n""", DisplayName = "Newline")] + [DataRow("tab\there\0", @"""tab\there\0""", DisplayName = "Tab and null")] + [DataRow("bell\u0007 sep\u2028", @"""bell\a sep\u2028""", DisplayName = "Other control and separator")] + public void ParameterEscapesAQuotedDefaultValue(string value, string expectedLiteral) => + Assert.AreEqual( + $"string name = {expectedLiteral}", + Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = value, DefaultValueIsQuoted = true })); + [TestMethod] public void ParameterKeepsItsAttributesOnTheDeclarationLine() { @@ -59,6 +70,12 @@ public void FieldWritesItsInitialiser() => "private const int Max = 10;\n", Render(new FieldTemplate { Type = "int", Name = "Max", Keywords = { "private", "const" }, DefaultValue = "10" })); + [TestMethod] + public void FieldEscapesAQuotedInitialiser() => + Assert.AreEqual( + "private string p = \"C:\\\\temp\\\\\\\"x\\\"\";\n", + Render(new FieldTemplate { Type = "string", Name = "p", Keywords = { "private" }, DefaultValue = "C:\\temp\\\"x\"", DefaultValueIsQuoted = true })); + [TestMethod] public void FieldCommentsAndAttributesGoOnTheirOwnLines() { diff --git a/CodeBlocker/Templates/TemplateBase.cs b/CodeBlocker/Templates/TemplateBase.cs index 42349ac..ee31222 100644 --- a/CodeBlocker/Templates/TemplateBase.cs +++ b/CodeBlocker/Templates/TemplateBase.cs @@ -27,9 +27,9 @@ public abstract class TemplateBase public string DefaultValue { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether is written inside double - /// quotes. Set this for string literals; leave it clear for anything already written as an - /// expression. + /// Gets or sets a value indicating whether is written as a string + /// literal. Set this to pass the raw text of a string, which is escaped and quoted; leave it clear + /// for anything already written as an expression. /// public bool DefaultValueIsQuoted { get; set; } @@ -79,8 +79,8 @@ public virtual void WriteTo(CodeBlocker codeBlocker) } /// - /// Writes = value when is set, quoting it when - /// is set. + /// Writes = value when is set, as an escaped string literal + /// when is set. /// /// The to write to. protected void WriteDefaultValueTo(CodeBlocker codeBlocker) @@ -93,7 +93,7 @@ protected void WriteDefaultValueTo(CodeBlocker codeBlocker) } codeBlocker.Write(" = "); - codeBlocker.Write(DefaultValueIsQuoted ? $"\"{DefaultValue}\"" : DefaultValue); + codeBlocker.Write(DefaultValueIsQuoted ? TemplateRendering.QuoteStringLiteral(DefaultValue) : DefaultValue); } } diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs index c54a2c4..3fe9451 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -2,6 +2,9 @@ namespace ktsu.CodeBlocker.Templates; +using System.Globalization; +using System.Text; + /// /// The rendering steps shared by more than one template. /// @@ -82,6 +85,54 @@ internal static void SpliceFragment(CodeBlocker parent, string fragment) } } + /// + /// Formats raw text as a C# regular string literal, quoted and escaped so that it compiles and + /// evaluates back to exactly . + /// + /// The raw text of the string. + /// The quoted literal. + /// + /// Backslashes, quotes and the common control characters take their short escapes; any other + /// control character, and the line and paragraph separators the compiler also treats as line + /// breaks, take a \uXXXX escape. + /// + internal static string QuoteStringLiteral(string value) + { + StringBuilder literal = new(value.Length + 2); + literal.Append('"'); + foreach (char c in value) + { + string? escape = c switch + { + '\\' => @"\\", + '"' => "\\\"", + '\0' => @"\0", + '\a' => @"\a", + '\b' => @"\b", + '\f' => @"\f", + '\n' => @"\n", + '\r' => @"\r", + '\t' => @"\t", + '\v' => @"\v", + _ when char.IsControl(c) || c is '\u2028' or '\u2029' => + @"\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture), + _ => null, + }; + + if (escape is null) + { + literal.Append(c); + } + else + { + literal.Append(escape); + } + } + + literal.Append('"'); + return literal.ToString(); + } + /// /// Writes a parenthesised, comma-separated parameter list. ///