Skip to content

Commit

Permalink
Mimic Full Framework string quoting (#1369)
Browse files Browse the repository at this point in the history
* Mimic Full Framework string quoting

CodeDom is not available on .net core, so copy over its string quoting
implementation.
Ideally, all this code should be replaced with crossplatform Roselyn

Fixes #1294
  • Loading branch information
cdmihai authored Nov 22, 2016
1 parent a1e9dcd commit 5480712
Show file tree
Hide file tree
Showing 2 changed files with 293 additions and 4 deletions.
80 changes: 80 additions & 0 deletions src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,86 @@ public void OneAttributeTwoPositionalParams()
File.Delete(task.OutputFile.ItemSpec);
}

public static string EscapedLineSeparator => NativeMethodsShared.IsWindows ? "\\r\\n" : "\\n";

/// <summary>
/// Multi line argument values should cause a verbatim string to be used
/// </summary>
[Fact]
public void MultilineAttributeCSharp()
{
var lines = new[] { "line 1", "line 2", "line 3" };
var multilineString = String.Join(Environment.NewLine, lines);

WriteCodeFragment task = new WriteCodeFragment();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
attribute.SetMetadata("_Parameter1", multilineString);
attribute.SetMetadata("Description", multilineString);
task.AssemblyAttributes = new TaskItem[] { attribute };
task.Language = "c#";
task.OutputDirectory = new TaskItem(Path.GetTempPath());
bool result = task.Execute();

Assert.Equal(true, result);

string content = File.ReadAllText(task.OutputFile.ItemSpec);
Console.WriteLine(content);

var csMultilineString = lines.Aggregate((l1, l2) => l1 + EscapedLineSeparator + l2);
CheckContentCSharp(content, $"[assembly: System.Reflection.AssemblyDescriptionAttribute(\"{csMultilineString}\", Description=\"{csMultilineString}\")]");

File.Delete(task.OutputFile.ItemSpec);
}

private static readonly string VBCarriageReturn = "Global.Microsoft.VisualBasic.ChrW(13)";
private static readonly string VBLineFeed = "Global.Microsoft.VisualBasic.ChrW(10)";
private static readonly string WindowsNewLine = $"{VBCarriageReturn}&{VBLineFeed}";

public static readonly string VBLineSeparator =
#if FEATURE_CODEDOM
WindowsNewLine;
#else
NativeMethodsShared.IsWindows
? WindowsNewLine
: VBLineFeed;
#endif

/// <summary>
/// Multi line argument values should cause a verbatim string to be used
/// </summary>
[Fact]
public void MultilineAttributeVB()
{
var lines = new []{ "line 1", "line 2", "line 3" };
var multilineString = String.Join(Environment.NewLine, lines);

WriteCodeFragment task = new WriteCodeFragment();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
attribute.SetMetadata("_Parameter1", multilineString);
attribute.SetMetadata("Description", multilineString);
task.AssemblyAttributes = new TaskItem[] { attribute };
task.Language = "visualbasic";
task.OutputDirectory = new TaskItem(Path.GetTempPath());
bool result = task.Execute();

Assert.Equal(true, result);

string content = File.ReadAllText(task.OutputFile.ItemSpec);
Console.WriteLine(content);

var vbMultilineString = lines
.Select(l => $"\"{l}\"")
.Aggregate((l1, l2) => $"{l1}&{VBLineSeparator}&{l2}");

CheckContentVB(content, $"<Assembly: System.Reflection.AssemblyDescriptionAttribute({vbMultilineString}, Description:={vbMultilineString})>");

File.Delete(task.OutputFile.ItemSpec);
}

/// <summary>
/// Some attributes only allow positional constructor arguments.
/// To set those, use metadata names like "_Parameter1", "_Parameter2" etc.
Expand Down
217 changes: 213 additions & 4 deletions src/XMakeTasks/WriteCodeFragment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ private string GenerateCodeCoreClr(out string extension)

foreach (ITaskItem attributeItem in AssemblyAttributes)
{
string args = GetAttributeArguments(attributeItem, "=");
string args = GetAttributeArguments(attributeItem, "=", QuoteSnippetStringCSharp);
if (args == null) return null;

code.AppendLine(string.Format($"[assembly: {attributeItem.ItemSpec}({args})]"));
Expand All @@ -326,7 +326,7 @@ private string GenerateCodeCoreClr(out string extension)

foreach (ITaskItem attributeItem in AssemblyAttributes)
{
string args = GetAttributeArguments(attributeItem, ":=");
string args = GetAttributeArguments(attributeItem, ":=", QuoteSnippetStringVisualBasic);
if (args == null) return null;

code.AppendLine(string.Format($"<Assembly: {attributeItem.ItemSpec}({args})>"));
Expand All @@ -343,7 +343,7 @@ private string GenerateCodeCoreClr(out string extension)
return haveGeneratedContent ? code.ToString() : string.Empty;
}

private string GetAttributeArguments(ITaskItem attributeItem, string namedArgumentString)
private string GetAttributeArguments(ITaskItem attributeItem, string namedArgumentString, Func<string, string> quoteString)
{
// Some attributes only allow positional constructor arguments, or the user may just prefer them.
// To set those, use metadata names like "_Parameter1", "_Parameter2" etc.
Expand All @@ -357,7 +357,7 @@ private string GetAttributeArguments(ITaskItem attributeItem, string namedArgume
foreach (DictionaryEntry entry in customMetadata)
{
string name = (string) entry.Key;
string value = entry.Value is string ? $@"""{entry.Value}""" : entry.Value.ToString();
string value = entry.Value is string ? quoteString(entry.Value.ToString()) : entry.Value.ToString();

if (name.StartsWith("_Parameter", StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -404,5 +404,214 @@ private string GetAttributeArguments(ITaskItem attributeItem, string namedArgume

return string.Join(", ", orderedParameters.Union(namedParameters).Where(p => !string.IsNullOrWhiteSpace(p)));
}

private const int MaxLineLength = 80;

// copied from Microsoft.CSharp.CSharpCodeProvider
private string QuoteSnippetStringCSharp(string value)
{
// If the string is short, use C style quoting (e.g "\r\n")
// Also do it if it is too long to fit in one line
// If the string contains '\0', verbatim style won't work.
if (value.Length < 256 || value.Length > 1500 || (value.IndexOf('\0') != -1))
return QuoteSnippetStringCStyle(value);

// Otherwise, use 'verbatim' style quoting (e.g. @"foo")
return QuoteSnippetStringVerbatimStyle(value);
}

// copied from Microsoft.CSharp.CSharpCodeProvider
private string QuoteSnippetStringCStyle(string value)
{
StringBuilder b = new StringBuilder(value.Length + 5);

b.Append("\"");

int i = 0;
while (i < value.Length)
{
switch (value[i])
{
case '\r':
b.Append("\\r");
break;
case '\t':
b.Append("\\t");
break;
case '\"':
b.Append("\\\"");
break;
case '\'':
b.Append("\\\'");
break;
case '\\':
b.Append("\\\\");
break;
case '\0':
b.Append("\\0");
break;
case '\n':
b.Append("\\n");
break;
case '\u2028':
case '\u2029':
b.Append("\\n");
break;

default:
b.Append(value[i]);
break;
}

if (i > 0 && i%MaxLineLength == 0)
{
//
// If current character is a high surrogate and the following
// character is a low surrogate, don't break them.
// Otherwise when we write the string to a file, we might lose
// the characters.
//
if (Char.IsHighSurrogate(value[i])
&& (i < value.Length - 1)
&& Char.IsLowSurrogate(value[i + 1]))
{
b.Append(value[++i]);
}

b.Append("\" +");
b.Append(Environment.NewLine);
b.Append('\"');
}
++i;
}

b.Append("\"");

return b.ToString();
}

// copied from Microsoft.CSharp.CSharpCodeProvider
private string QuoteSnippetStringVerbatimStyle(string value)
{
StringBuilder b = new StringBuilder(value.Length + 5);

b.Append("@\"");

for (int i = 0; i < value.Length; i++)
{
if (value[i] == '\"')
b.Append("\"\"");
else
b.Append(value[i]);
}

b.Append("\"");

return b.ToString();
}

// copied from Microsoft.VisualBasic.VBCodeProvider
private string QuoteSnippetStringVisualBasic(string value)
{
StringBuilder b = new StringBuilder(value.Length + 5);

bool fInDoubleQuotes = true;

b.Append("\"");

int i = 0;
while (i < value.Length)
{
char ch = value[i];
switch (ch)
{
case '\"':
// These are the inward sloping quotes used by default in some cultures like CHS.
// VBC.EXE does a mapping ANSI that results in it treating these as syntactically equivalent to a
// regular double quote.
case '\u201C':
case '\u201D':
case '\uFF02':
EnsureInDoubleQuotes(ref fInDoubleQuotes, b);
b.Append(ch);
b.Append(ch);
break;
case '\r':
EnsureNotInDoubleQuotes(ref fInDoubleQuotes, b);
if (i < value.Length - 1 && value[i + 1] == '\n')
{
b.Append("&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)");
i++;
}
else
{
b.Append("&Global.Microsoft.VisualBasic.ChrW(13)");
}
break;
case '\t':
EnsureNotInDoubleQuotes(ref fInDoubleQuotes, b);
b.Append("&Global.Microsoft.VisualBasic.ChrW(9)");
break;
case '\0':
EnsureNotInDoubleQuotes(ref fInDoubleQuotes, b);
b.Append("&Global.Microsoft.VisualBasic.ChrW(0)");
break;
case '\n':
case '\u2028':
case '\u2029':
EnsureNotInDoubleQuotes(ref fInDoubleQuotes, b);
b.Append("&Global.Microsoft.VisualBasic.ChrW(10)");
break;
default:
EnsureInDoubleQuotes(ref fInDoubleQuotes, b);
b.Append(value[i]);
break;
}

if (i > 0 && i%MaxLineLength == 0)
{
//
// If current character is a high surrogate and the following
// character is a low surrogate, don't break them.
// Otherwise when we write the string to a file, we might lose
// the characters.
//
if (Char.IsHighSurrogate(value[i])
&& (i < value.Length - 1)
&& Char.IsLowSurrogate(value[i + 1]))
{
b.Append(value[++i]);
}

if (fInDoubleQuotes)
b.Append("\"");
fInDoubleQuotes = true;

b.Append("& _ ");
b.Append(Environment.NewLine);
b.Append('\"');
}
++i;
}

if (fInDoubleQuotes)
b.Append("\"");

return b.ToString();
}

private void EnsureNotInDoubleQuotes(ref bool fInDoubleQuotes, StringBuilder b)
{
if (!fInDoubleQuotes) return;
b.Append("\"");
fInDoubleQuotes = false;
}

private void EnsureInDoubleQuotes(ref bool fInDoubleQuotes, StringBuilder b)
{
if (fInDoubleQuotes) return;
b.Append("&\"");
fInDoubleQuotes = true;
}
}
}

0 comments on commit 5480712

Please sign in to comment.