Mono: Make sure the generated RootNamespace is a valid identifier

This commit is contained in:
Ignacio Etcheverry 2019-02-28 22:24:31 +01:00
parent b69569415f
commit 5a4475fce3
3 changed files with 204 additions and 1 deletions

View file

@ -41,6 +41,7 @@
<Compile Include="Build\BuildSystem.cs" /> <Compile Include="Build\BuildSystem.cs" />
<Compile Include="Editor\MonoDevelopInstance.cs" /> <Compile Include="Editor\MonoDevelopInstance.cs" />
<Compile Include="Project\ProjectExtensions.cs" /> <Compile Include="Project\ProjectExtensions.cs" />
<Compile Include="Project\IdentifierUtils.cs" />
<Compile Include="Project\ProjectGenerator.cs" /> <Compile Include="Project\ProjectGenerator.cs" />
<Compile Include="Project\ProjectUtils.cs" /> <Compile Include="Project\ProjectUtils.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />

View file

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace GodotSharpTools.Project
{
public static class IdentifierUtils
{
public static string SanitizeQualifiedIdentifier(string qualifiedIdentifier, bool allowEmptyIdentifiers)
{
if (string.IsNullOrEmpty(qualifiedIdentifier))
throw new ArgumentException($"{nameof(qualifiedIdentifier)} cannot be empty", nameof(qualifiedIdentifier));
string[] identifiers = qualifiedIdentifier.Split(new[] { '.' });
for (int i = 0; i < identifiers.Length; i++)
{
identifiers[i] = SanitizeIdentifier(identifiers[i], allowEmpty: allowEmptyIdentifiers);
}
return string.Join(".", identifiers);
}
public static string SanitizeIdentifier(string identifier, bool allowEmpty)
{
if (string.IsNullOrEmpty(identifier))
{
if (allowEmpty)
return "Empty"; // Default value for empty identifiers
throw new ArgumentException($"{nameof(identifier)} cannot be empty if {nameof(allowEmpty)} is false", nameof(identifier));
}
if (identifier.Length > 511)
identifier = identifier.Substring(0, 511);
var identifierBuilder = new StringBuilder();
int startIndex = 0;
if (identifier[0] == '@')
{
identifierBuilder.Append('@');
startIndex += 1;
}
for (int i = startIndex; i < identifier.Length; i++)
{
char @char = identifier[i];
switch (Char.GetUnicodeCategory(@char))
{
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.ModifierLetter:
case UnicodeCategory.LetterNumber:
case UnicodeCategory.OtherLetter:
identifierBuilder.Append(@char);
break;
case UnicodeCategory.NonSpacingMark:
case UnicodeCategory.SpacingCombiningMark:
case UnicodeCategory.ConnectorPunctuation:
case UnicodeCategory.DecimalDigitNumber:
// Identifiers may start with underscore
if (identifierBuilder.Length > startIndex || @char == '_')
identifierBuilder.Append(@char);
break;
default:
break;
}
}
if (identifierBuilder.Length == startIndex)
{
// All characters were invalid so now it's empty. Fill it with something.
identifierBuilder.Append("Empty");
}
identifier = identifierBuilder.ToString();
if (identifier[0] != '@' && IsKeyword(identifier, anyDoubleUnderscore: true))
identifier = '@' + identifier;
return identifier;
}
static bool IsKeyword(string value, bool anyDoubleUnderscore)
{
// Identifiers that start with double underscore are meant to be used for reserved keywords.
// Only existing keywords are enforced, but it may be useful to forbid any identifier
// that begins with double underscore to prevent issues with future C# versions.
if (anyDoubleUnderscore)
{
if (value.Length > 2 && value[0] == '_' && value[1] == '_' && value[2] != '_')
return true;
}
else
{
if (_doubleUnderscoreKeywords.Contains(value))
return true;
}
return _keywords.Contains(value);
}
static HashSet<string> _doubleUnderscoreKeywords = new HashSet<string>
{
"__arglist",
"__makeref",
"__reftype",
"__refvalue",
};
static HashSet<string> _keywords = new HashSet<string>
{
"as",
"do",
"if",
"in",
"is",
"for",
"int",
"new",
"out",
"ref",
"try",
"base",
"bool",
"byte",
"case",
"char",
"else",
"enum",
"goto",
"lock",
"long",
"null",
"this",
"true",
"uint",
"void",
"break",
"catch",
"class",
"const",
"event",
"false",
"fixed",
"float",
"sbyte",
"short",
"throw",
"ulong",
"using",
"where",
"while",
"yield",
"double",
"extern",
"object",
"params",
"public",
"return",
"sealed",
"sizeof",
"static",
"string",
"struct",
"switch",
"typeof",
"unsafe",
"ushort",
"checked",
"decimal",
"default",
"finally",
"foreach",
"partial",
"private",
"virtual",
"abstract",
"continue",
"delegate",
"explicit",
"implicit",
"internal",
"operator",
"override",
"readonly",
"volatile",
"interface",
"namespace",
"protected",
"unchecked",
"stackalloc",
};
}
}

View file

@ -140,6 +140,9 @@ namespace GodotSharpTools.Project
public static ProjectRootElement CreateLibraryProject(string name, out ProjectPropertyGroupElement mainGroup) public static ProjectRootElement CreateLibraryProject(string name, out ProjectPropertyGroupElement mainGroup)
{ {
if (string.IsNullOrEmpty(name))
throw new ArgumentException($"{nameof(name)} cannot be empty", nameof(name));
var root = ProjectRootElement.Create(); var root = ProjectRootElement.Create();
root.DefaultTargets = "Build"; root.DefaultTargets = "Build";
@ -149,7 +152,7 @@ namespace GodotSharpTools.Project
mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}"); mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}");
mainGroup.AddProperty("OutputType", "Library"); mainGroup.AddProperty("OutputType", "Library");
mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)")); mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)"));
mainGroup.AddProperty("RootNamespace", name); mainGroup.AddProperty("RootNamespace", IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true));
mainGroup.AddProperty("AssemblyName", name); mainGroup.AddProperty("AssemblyName", name);
mainGroup.AddProperty("TargetFrameworkVersion", "v4.5"); mainGroup.AddProperty("TargetFrameworkVersion", "v4.5");