Skip to content

Commit

Permalink
Mimic Full Framework string quoting
Browse files Browse the repository at this point in the history
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 dotnet#1294
  • Loading branch information
cdmihai committed Nov 21, 2016
1 parent c8aeb00 commit e609bdc
Show file tree
Hide file tree
Showing 2 changed files with 303 additions and 7 deletions.
88 changes: 85 additions & 3 deletions src/XMakeTasks/UnitTests/WriteCodeFragment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
using Microsoft.Build.Shared;
using Xunit;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using PlatformID = Xunit.PlatformID;

namespace Microsoft.Build.UnitTests
{
Expand Down Expand Up @@ -198,7 +200,7 @@ public void InvalidFilePath()
/// Bad directory path
/// </summary>
[Fact]
[PlatformSpecific(Xunit.PlatformID.Windows)] // "No invalid characters on Unix"
[PlatformSpecific(PlatformID.Windows)] // "No invalid characters on Unix"
public void InvalidDirectoryPath()
{
WriteCodeFragment task = new WriteCodeFragment();
Expand Down Expand Up @@ -429,6 +431,86 @@ public void OneAttributeTwoPositionalParams()
File.Delete(task.OutputFile.ItemSpec);
}

public static string EscapedLineSeparator => (NativeMethodsShared.IsWindows ? "\\r" : "") + "\\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 Expand Up @@ -582,10 +664,10 @@ private static void CheckContentVB(string actualContent, params string[] expecte

private static void CheckContent(string actualContent, string[] expectedAttributes, string commentStart, params string[] expectedHeader)
{
string expectedContent = string.Join(Environment.NewLine, expectedHeader.Concat(expectedAttributes));
string expectedContent = String.Join(Environment.NewLine, expectedHeader.Concat(expectedAttributes));

// we tolerate differences in whitespace and comments between platforms
string normalizedActualContent = string.Join(
string normalizedActualContent = String.Join(
Environment.NewLine,
actualContent.Split('\r', '\n')
.Select(line => line.Trim())
Expand Down
222 changes: 218 additions & 4 deletions src/XMakeTasks/WriteCodeFragment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ private string GenerateCode(out string extension)
// as there's no point writing the file
return haveGeneratedContent ? code : String.Empty;
}

private class CodePrimitiveExpressionWithBetterMultilineStringSupport : CodePrimitiveExpression
{

}
#endif

/// <summary>
Expand Down Expand Up @@ -303,7 +308,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 +331,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 +348,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 +362,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 +409,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 e609bdc

Please sign in to comment.