Skip to content

Commit ad81b8a

Browse files
committed
#3241: Namespaces in generated source must be normalized against invalid characters
1 parent 8592d73 commit ad81b8a

3 files changed

Lines changed: 194 additions & 3 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// This source is copied from
2+
// https://github.com/microsoft/testfx/blob/3564400fca5863f716d89fc84326e420f0b33d1a/src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/NamespaceHelpers.cs
3+
// because we need the namespace to be identical to what's generated in SelfRegisteredExtensions.cs
4+
//
5+
// Licensed by Microsoft under the MIT license. https://github.com/microsoft/testfx/blob/3564400fca5863f716d89fc84326e420f0b33d1a/LICENSE
6+
7+
using System.Text;
8+
9+
namespace Xunit.Internal;
10+
11+
internal static class NamespaceHelpers
12+
{
13+
// https://github.com/dotnet/templating/blob/b0b1283f8c96be35f1b65d4b0c1ec0534d86fc2f/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/DefaultSafeNamespaceValueFormFactory.cs#L17-L59
14+
internal static string ToSafeNamespace(string value)
15+
{
16+
const char invalidCharacterReplacement = '_';
17+
18+
value = value.Trim();
19+
20+
StringBuilder safeValueStr = new(value.Length);
21+
22+
for (int i = 0; i < value.Length; i++)
23+
{
24+
if (i < value.Length - 1 && char.IsSurrogatePair(value[i], value[i + 1]))
25+
{
26+
safeValueStr.Append(invalidCharacterReplacement);
27+
// Skip both chars that make up this symbol.
28+
i++;
29+
continue;
30+
}
31+
32+
bool isFirstCharacterOfIdentifier = safeValueStr.Length == 0 || safeValueStr[safeValueStr.Length - 1] == '.';
33+
bool isValidFirstCharacter = UnicodeCharacterUtilities.IsIdentifierStartCharacter(value[i]);
34+
bool isValidPartCharacter = UnicodeCharacterUtilities.IsIdentifierPartCharacter(value[i]);
35+
36+
if (isFirstCharacterOfIdentifier && !isValidFirstCharacter && isValidPartCharacter)
37+
{
38+
// This character cannot be at the beginning, but is good otherwise. Prefix it with something valid.
39+
safeValueStr.Append(invalidCharacterReplacement);
40+
safeValueStr.Append(value[i]);
41+
}
42+
else if ((isFirstCharacterOfIdentifier && isValidFirstCharacter) ||
43+
(!isFirstCharacterOfIdentifier && isValidPartCharacter) ||
44+
(safeValueStr.Length > 0 && i < value.Length - 1 && value[i] == '.'))
45+
{
46+
// This character is allowed to be where it is.
47+
safeValueStr.Append(value[i]);
48+
}
49+
else
50+
{
51+
safeValueStr.Append(invalidCharacterReplacement);
52+
}
53+
}
54+
55+
return safeValueStr.ToString();
56+
}
57+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// This source is copied from
2+
// https://github.com/microsoft/testfx/blob/3564400fca5863f716d89fc84326e420f0b33d1a/src/Analyzers/MSTest.SourceGeneration/Helpers/UnicodeCharacterUtilities.cs#L11
3+
// because we need the namespace to be identical to what's generated in SelfRegisteredExtensions.cs
4+
//
5+
// Licensed by Microsoft under the MIT license. https://github.com/microsoft/testfx/blob/3564400fca5863f716d89fc84326e420f0b33d1a/LICENSE
6+
7+
using System.Globalization;
8+
9+
namespace Xunit.Internal;
10+
11+
/// <summary>
12+
/// Defines a set of helper methods to classify Unicode characters.
13+
/// </summary>
14+
internal static class UnicodeCharacterUtilities
15+
{
16+
public static bool IsIdentifierStartCharacter(char ch)
17+
{
18+
// identifier-start-character:
19+
// letter-character
20+
// _ (the underscore character U+005F)
21+
if (ch < 'a') // '\u0061'
22+
{
23+
if (ch < 'A') // '\u0041'
24+
{
25+
return false;
26+
}
27+
28+
return ch is <= 'Z' // '\u005A'
29+
or '_'; // '\u005F'
30+
}
31+
32+
if (ch <= 'z') // '\u007A'
33+
{
34+
return true;
35+
}
36+
37+
if (ch <= '\u007F') // max ASCII
38+
{
39+
return false;
40+
}
41+
42+
// Check if letter-character
43+
return IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch));
44+
}
45+
46+
/// <summary>
47+
/// Returns true if the Unicode character can be a part of an identifier.
48+
/// </summary>
49+
/// <param name="ch">The Unicode character.</param>
50+
public static bool IsIdentifierPartCharacter(char ch)
51+
{
52+
// identifier-part-character:
53+
// letter-character
54+
// decimal-digit-character
55+
// connecting-character
56+
// combining-character
57+
// formatting-character
58+
if (ch < 'a') // '\u0061'
59+
{
60+
if (ch < 'A') // '\u0041'
61+
{
62+
return ch is >= '0' // '\u0030'
63+
and <= '9'; // '\u0039'
64+
}
65+
66+
return ch is <= 'Z' // '\u005A'
67+
or '_'; // '\u005F'
68+
}
69+
70+
if (ch <= 'z') // '\u007A'
71+
{
72+
return true;
73+
}
74+
75+
if (ch <= '\u007F') // max ASCII
76+
{
77+
return false;
78+
}
79+
80+
UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory(ch);
81+
return IsLetterChar(cat)
82+
|| IsDecimalDigitChar(cat)
83+
|| IsConnectingChar(cat)
84+
|| IsCombiningChar(cat)
85+
|| IsFormattingChar(cat);
86+
}
87+
88+
private static bool IsLetterChar(UnicodeCategory cat)
89+
// letter-character:
90+
// A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl
91+
// A Unicode-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl
92+
=> cat switch
93+
{
94+
UnicodeCategory.UppercaseLetter or UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter or UnicodeCategory.ModifierLetter or UnicodeCategory.OtherLetter or UnicodeCategory.LetterNumber => true,
95+
_ => false,
96+
};
97+
98+
private static bool IsCombiningChar(UnicodeCategory cat)
99+
// combining-character:
100+
// A Unicode character of classes Mn or Mc
101+
// A Unicode-escape-sequence representing a character of classes Mn or Mc
102+
=> cat switch
103+
{
104+
UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark => true,
105+
_ => false,
106+
};
107+
108+
private static bool IsDecimalDigitChar(UnicodeCategory cat)
109+
// decimal-digit-character:
110+
// A Unicode character of the class Nd
111+
// A unicode-escape-sequence representing a character of the class Nd
112+
=> cat == UnicodeCategory.DecimalDigitNumber;
113+
114+
private static bool IsConnectingChar(UnicodeCategory cat)
115+
// connecting-character:
116+
// A Unicode character of the class Pc
117+
// A unicode-escape-sequence representing a character of the class Pc
118+
=> cat == UnicodeCategory.ConnectorPunctuation;
119+
120+
/// <summary>
121+
/// Returns true if the Unicode character is a formatting character (Unicode class Cf).
122+
/// </summary>
123+
/// <param name="cat">The Unicode character.</param>
124+
private static bool IsFormattingChar(UnicodeCategory cat)
125+
// formatting-character:
126+
// A Unicode character of the class Cf
127+
// A unicode-escape-sequence representing a character of the class Cf
128+
=> cat == UnicodeCategory.Format;
129+
}

src/xunit.v3.msbuildtasks/XunitGenerateEntryPoint.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ public sealed class XunitGenerateEntryPoint : Task
1818
const string LanguageVB = "VB";
1919

2020
readonly HashSet<string> languages = new(StringComparer.OrdinalIgnoreCase) { LanguageCS, LanguageFS, LanguageVB };
21+
string? rootNamespace;
2122

2223
[Required]
2324
public ITaskItem Language { get; set; }
2425

25-
public string? RootNamespace { get; set; }
26+
public string RootNamespace
27+
{
28+
get => rootNamespace ?? string.Empty;
29+
set => rootNamespace = NamespaceHelpers.ToSafeNamespace(value ?? string.Empty);
30+
}
2631

2732
[Required]
2833
public ITaskItem SourcePath { get; set; }
@@ -49,13 +54,13 @@ public override bool Execute()
4954

5055
static void GenerateEntryPoint(
5156
string language,
52-
string? rootNamespace,
57+
string rootNamespace,
5358
bool useMicrosoftTestingPlatformRunner,
5459
ITaskItem sourcePath,
5560
TaskLoggingHelper log)
5661
{
5762
var @namespace =
58-
string.IsNullOrWhiteSpace(rootNamespace)
63+
rootNamespace.Length == 0
5964
? "AutoGenerated"
6065
: rootNamespace + ".AutoGenerated";
6166

0 commit comments

Comments
 (0)