123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.Text;
- namespace GodotTools.ProjectEditor
- {
- 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('.');
- for (int i = 0; i < identifiers.Length; i++)
- {
- identifiers[i] = SanitizeIdentifier(identifiers[i], allowEmpty: allowEmptyIdentifiers);
- }
- return string.Join(".", identifiers);
- }
- /// <summary>
- /// Skips invalid identifier characters including decimal digit numbers at the start of the identifier.
- /// </summary>
- private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder)
- {
- for (int i = startIndex; i < source.Length; i++)
- {
- char @char = source[i];
- switch (char.GetUnicodeCategory(@char))
- {
- case UnicodeCategory.UppercaseLetter:
- case UnicodeCategory.LowercaseLetter:
- case UnicodeCategory.TitlecaseLetter:
- case UnicodeCategory.ModifierLetter:
- case UnicodeCategory.LetterNumber:
- case UnicodeCategory.OtherLetter:
- outputBuilder.Append(@char);
- break;
- case UnicodeCategory.NonSpacingMark:
- case UnicodeCategory.SpacingCombiningMark:
- case UnicodeCategory.ConnectorPunctuation:
- case UnicodeCategory.DecimalDigitNumber:
- // Identifiers may start with underscore
- if (outputBuilder.Length > startIndex || @char == '_')
- outputBuilder.Append(@char);
- break;
- }
- }
- }
- 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;
- }
- SkipInvalidCharacters(identifier, startIndex, identifierBuilder);
- 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);
- }
- private static readonly HashSet<string> DoubleUnderscoreKeywords = new HashSet<string>
- {
- "__arglist",
- "__makeref",
- "__reftype",
- "__refvalue",
- };
- private static readonly 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",
- };
- }
- }
|