diff --git a/src/bgen/ExtensionMethods.cs b/src/bgen/ExtensionMethods.cs index e22885e17e..6cef6a9fa2 100644 --- a/src/bgen/ExtensionMethods.cs +++ b/src/bgen/ExtensionMethods.cs @@ -160,86 +160,3 @@ public static class ReflectionExtensions { return methods; } } - -// Fixes bug 27430 - btouch doesn't escape identifiers with the same name as C# keywords -public static class StringExtensions { - public static string? GetSafeParamName (this string? paramName) - { - if (paramName is null) - return paramName; - - if (!IsValidIdentifier (paramName, out var hasIllegalChars)) { - return hasIllegalChars ? null : "@" + paramName; - } - return paramName; - } - - // Since we're building against the iOS assemblies and there's no code generation there, - // I'm bringing the implementation from: - // mono/mcs/class//System/Microsoft.CSharp/CSharpCodeGenerator.cs - static bool IsValidIdentifier (string? identifier, out bool hasIllegalChars) - { - hasIllegalChars = false; - if (identifier is null || identifier.Length == 0) - return false; - - if (keywordsTable is null) - FillKeywordTable (); - - if (keywordsTable!.Contains (identifier)) - return false; - - if (!is_identifier_start_character (identifier [0])) { - // if we are dealing with a number, we are ok, we can prepend @, else we have a problem - hasIllegalChars = !Char.IsNumber (identifier [0]); - return false; - } - - for (int i = 1; i < identifier.Length; i++) - if (!is_identifier_part_character (identifier [i])) { - hasIllegalChars = true; - return false; - } - - return true; - } - - static bool is_identifier_start_character (char c) - { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '@' || Char.IsLetter (c); - } - - static bool is_identifier_part_character (char c) - { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9') || Char.IsLetter (c); - } - - static void FillKeywordTable () - { - lock (keywords) { - if (keywordsTable is null) { - keywordsTable = new Hashtable (); - foreach (string keyword in keywords) { - keywordsTable.Add (keyword, keyword); - } - } - } - } - - static Hashtable? keywordsTable; - - static string [] keywords = new string [] { - "abstract","event","new","struct","as","explicit","null","switch","base","extern", - "this","false","operator","throw","break","finally","out","true", - "fixed","override","try","case","params","typeof","catch","for", - "private","foreach","protected","checked","goto","public", - "unchecked","class","if","readonly","unsafe","const","implicit","ref", - "continue","in","return","using","virtual","default", - "interface","sealed","volatile","delegate","internal","do","is", - "sizeof","while","lock","stackalloc","else","static","enum", - "namespace", - "object","bool","byte","float","uint","char","ulong","ushort", - "decimal","int","sbyte","short","double","long","string","void", - "partial", "yield", "where" - }; -} diff --git a/src/bgen/Filters.cs b/src/bgen/Filters.cs index 57584195b5..004e2eb1da 100644 --- a/src/bgen/Filters.cs +++ b/src/bgen/Filters.cs @@ -215,7 +215,7 @@ public partial class Generator { if (sel.StartsWith ("input", StringComparison.Ordinal)) name = sel; else - name = "input" + Capitalize (sel); + name = "input" + sel.Capitalize (); } if (p.GetGetMethod () is not null) { @@ -239,7 +239,7 @@ public partial class Generator { var selector = export.Selector; if (setter) - selector = "set" + Capitalize (selector) + ":"; + selector = "set" + selector.Capitalize () + ":"; if (export.ArgumentSemantic != ArgumentSemantic.None && !p.PropertyType.IsPrimitive) print ($"[Export (\"{selector}\", ArgumentSemantic.{export.ArgumentSemantic})]"); diff --git a/src/bgen/Generator.cs b/src/bgen/Generator.cs index f1f1cca34b..4fa3b3a031 100644 --- a/src/bgen/Generator.cs +++ b/src/bgen/Generator.cs @@ -5815,7 +5815,7 @@ public partial class Generator : IMemberGatherer { if (p == null) return; - print ($"[Advice ({Quote (p.Message)})]"); + print ($"[Advice ({p.Message.Quote ()})]"); } public void PrintRequiresSuperAttribute (ICustomAttributeProvider mi) @@ -5833,7 +5833,7 @@ public partial class Generator : IMemberGatherer { if (p == null) return; - print ($"[NotImplemented ({Quote (p.Message)})]"); + print ($"[NotImplemented ({p.Message.Quote ()})]"); } public void PrintBindAsAttribute (ICustomAttributeProvider mi, StringBuilder sb = null) @@ -6849,7 +6849,7 @@ public partial class Generator : IMemberGatherer { var sender = pars.Length == 0 ? "this" : pars [0].Name; - var miname = PascalCase (mi.Name); + var miname = mi.Name.PascalCase (); if (miname == previous_miname) { // overloads, add a numbered suffix (it's internal) previous_miname = miname; @@ -6889,7 +6889,7 @@ public partial class Generator : IMemberGatherer { } else eaname = ""; - print ("var handler = {0};", PascalCase (miname)); + print ("var handler = {0};", miname.PascalCase ()); print ("if (handler != null){"); indent++; string eventArgs; @@ -6926,7 +6926,7 @@ public partial class Generator : IMemberGatherer { if (debug) print ("Console.WriteLine (\"Method {0}.{1} invoked\");", dtype.Name, mi.Name); - print ("var handler = {0};", PascalCase (miname)); + print ("var handler = {0};", miname.PascalCase ()); print ("if (handler != null)"); print (" return handler ({0}{1});", sender, @@ -6989,7 +6989,7 @@ public partial class Generator : IMemberGatherer { print ("if (selHandle.Equals (sel{0}Handle))", mi.Name); } ++indent; - print ("return {0} != null;", PascalCase (mi.Name)); + print ("return {0} != null;", mi.Name.PascalCase ()); --indent; } print ("return global::" + ns.Messaging + ".bool_objc_msgSendSuper_IntPtr (SuperHandle, " + selRespondsToSelector + ", selHandle);"); @@ -7023,7 +7023,7 @@ public partial class Generator : IMemberGatherer { string ensureArg = bta.KeepRefUntil == null ? "" : "this"; - var miname = PascalCase (mi.Name); + var miname = mi.Name.PascalCase (); if (miname == prev_miname) { // overloads, add a numbered suffix (it's internal) prev_miname = miname; @@ -7035,14 +7035,14 @@ public partial class Generator : IMemberGatherer { PrintObsoleteAttributes (mi); if (bta.Singleton && mi.GetParameters ().Length == 0 || mi.GetParameters ().Length == 1) - print ("public event EventHandler {0} {{", CamelCase (GetEventName (mi))); + print ("public event EventHandler {0} {{", GetEventName (mi).CamelCase ()); else - print ("public event EventHandler<{0}> {1} {{", GetEventArgName (mi), CamelCase (GetEventName (mi))); + print ("public event EventHandler<{0}> {1} {{", GetEventArgName (mi), GetEventName (mi).CamelCase ()); print ("\tadd {{ Ensure{0} ({1})!.{2} += value; }}", dtype.Name, ensureArg, miname); print ("\tremove {{ Ensure{0} ({1})!.{2} -= value; }}", dtype.Name, ensureArg, miname); print ("}\n"); } else { - print ("public {0}? {1} {{", GetDelegateName (mi), CamelCase (GetDelegateApiName (mi))); + print ("public {0}? {1} {{", GetDelegateName (mi), GetDelegateApiName (mi).CamelCase ()); print ("\tget {{ return Ensure{0} ({1})!.{2}; }}", dtype.Name, ensureArg, miname); print ("\tset {{ Ensure{0} ({1})!.{2} = value; }}", dtype.Name, ensureArg, miname); print ("}\n"); @@ -7269,7 +7269,7 @@ public partial class Generator : IMemberGatherer { var safe_name = pi.Name.GetSafeParamName (); print ("public {0} {1} {{ get; set; }}", FormatType (type, pi.ParameterType), - Capitalize (safe_name)); + safe_name.Capitalize ()); if (comma) ctor.Append (", "); @@ -7282,7 +7282,7 @@ public partial class Generator : IMemberGatherer { print ("\npublic {0} ({1}) {{", async_type.Item1, ctor); indent++; foreach (var pi in async_type.Item2) { var safe_name = pi.Name.GetSafeParamName (); - print ("this.{0} = {1};", Capitalize (safe_name), safe_name); + print ("this.{0} = {1};", safe_name.Capitalize (), safe_name); } print ("Initialize ();"); indent--; print ("}"); @@ -7450,14 +7450,6 @@ public partial class Generator : IMemberGatherer { return null; } - static string Capitalize (string str) - { - if (str.StartsWith ("@", StringComparison.Ordinal)) - return char.ToUpper (str [1]) + str.Substring (2); - - return char.ToUpper (str [0]) + str.Substring (1); - } - string GetNotificationCenter (PropertyInfo pi) { var a = AttributeManager.GetCustomAttributes (pi); @@ -7512,10 +7504,10 @@ public partial class Generator : IMemberGatherer { { var attrs = AttributeManager.GetCustomAttributes (pi); if (attrs.Length == 0) - return CamelCase (pi.Name).GetSafeParamName (); + return pi.Name.CamelCase ().GetSafeParamName (); var a = attrs [0]; - return CamelCase (a.EvtName).GetSafeParamName (); + return a.EvtName.CamelCase ().GetSafeParamName (); } string RenderArgs (IEnumerable pi) @@ -7533,16 +7525,6 @@ public partial class Generator : IMemberGatherer { return parameters.Any (pi => pi.ParameterType.IsByRef); } - string CamelCase (string ins) - { - return Char.ToUpper (ins [0]) + ins.Substring (1); - } - - string PascalCase (string ins) - { - return Char.ToLower (ins [0]) + ins.Substring (1); - } - Dictionary skipGeneration = new Dictionary (); string GetEventName (MethodInfo mi) { @@ -7720,16 +7702,6 @@ public partial class Generator : IMemberGatherer { } - public static string Quote (string s) - { - if (s == null) - return String.Empty; - if (s == string.Empty) - return @""""""; - - return $"@\"{s.Replace ("\"", "\"\"")}\""; - } - private static string FormatPropertyInfo (PropertyInfo pi) { return pi.DeclaringType.FullName + " " + pi.Name; diff --git a/src/bgen/StringExtensions.cs b/src/bgen/StringExtensions.cs new file mode 100644 index 0000000000..ca5294c2ef --- /dev/null +++ b/src/bgen/StringExtensions.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; + +#nullable enable + +// Fixes bug 27430 - btouch doesn't escape identifiers with the same name as C# keywords +public static class StringExtensions { + + public static string Quote (this string? self) + { + return self switch { + null => String.Empty, + "" => @"""""", + _ => $"@\"{self.Replace ("\"", "\"\"")}\"" + }; + } + + public static string CamelCase (this string ins) + { + return Char.ToUpper (ins [0]) + ins.Substring (1); + } + + public static string PascalCase (this string ins) + { + return Char.ToLower (ins [0]) + ins.Substring (1); + } + + public static string Capitalize (this string str) + { + if (str.StartsWith ("@", StringComparison.Ordinal)) + return char.ToUpper (str [1]) + str.Substring (2); + + return char.ToUpper (str [0]) + str.Substring (1); + } + + public static string? GetSafeParamName (this string? paramName) + { + if (paramName is null) + return paramName; + + if (!IsValidIdentifier (paramName, out var hasIllegalChars)) { + return hasIllegalChars ? null : "@" + paramName; + } + return paramName; + } + + // Since we're building against the iOS assemblies and there's no code generation there, + // I'm bringing the implementation from: + // mono/mcs/class//System/Microsoft.CSharp/CSharpCodeGenerator.cs + static bool IsValidIdentifier (string? identifier, out bool hasIllegalChars) + { + hasIllegalChars = false; + if (String.IsNullOrEmpty (identifier)) + return false; + + if (keywordsTable is null) + FillKeywordTable (); + + if (keywordsTable!.Contains (identifier)) + return false; + + if (!is_identifier_start_character (identifier [0])) { + // if we are dealing with a number, we are ok, we can prepend @, else we have a problem + hasIllegalChars = !Char.IsNumber (identifier [0]); + return false; + } + + for (int i = 1; i < identifier.Length; i++) + if (!is_identifier_part_character (identifier [i])) { + hasIllegalChars = true; + return false; + } + + return true; + } + + static bool is_identifier_start_character (char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '@' || Char.IsLetter (c); + } + + static bool is_identifier_part_character (char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9') || Char.IsLetter (c); + } + + static void FillKeywordTable () + { + lock (keywords) { + if (keywordsTable is null) { + keywordsTable = new Hashtable (); + foreach (string keyword in keywords) { + keywordsTable.Add (keyword, keyword); + } + } + } + } + + static Hashtable? keywordsTable; + + static string [] keywords = new string [] { + "abstract","event","new","struct","as","explicit","null","switch","base","extern", + "this","false","operator","throw","break","finally","out","true", + "fixed","override","try","case","params","typeof","catch","for", + "private","foreach","protected","checked","goto","public", + "unchecked","class","if","readonly","unsafe","const","implicit","ref", + "continue","in","return","using","virtual","default", + "interface","sealed","volatile","delegate","internal","do","is", + "sizeof","while","lock","stackalloc","else","static","enum", + "namespace", + "object","bool","byte","float","uint","char","ulong","ushort", + "decimal","int","sbyte","short","double","long","string","void", + "partial", "yield", "where" + }; +} diff --git a/src/generator.csproj b/src/generator.csproj index b9a2341cc0..f6e5248e95 100644 --- a/src/generator.csproj +++ b/src/generator.csproj @@ -114,6 +114,7 @@ + diff --git a/tests/generator/StringExtensionTests.cs b/tests/generator/StringExtensionTests.cs index 1f648e92be..0db37c9fe4 100644 --- a/tests/generator/StringExtensionTests.cs +++ b/tests/generator/StringExtensionTests.cs @@ -1,6 +1,9 @@ +using System; using System.Collections; using NUnit.Framework; +#nullable enable + namespace GeneratorTests { public class StringExtensionTests { @@ -45,5 +48,46 @@ namespace GeneratorTests { Assert.IsNotNull (legal, "legal != null"); Assert.AreEqual ("@" + illegal, legal, "legal"); } + + [Test] + public void QuoteNullString () + { + string? str = null; + Assert.AreEqual (string.Empty, str.Quote ()); + } + + [Test] + public void QuoteEmptyString () + { + string str = String.Empty; + Assert.AreEqual (@"""""", str.Quote ()); + } + + [TestCase ("No quotes", "@\"No quotes\"")] + [TestCase ("\"quotes\"", "@\"\"\"quotes\"\"\"")] + public void QuoteString (string input, string output) + { + Assert.AreEqual (output, input.Quote ()); + } + + [Test] + public void CamelCaseTest () + { + var str = "pascalCaseExample"; + Assert.AreEqual ("PascalCaseExample", str.CamelCase ()); + } + + [Test] + public void PascalCaseTest () + { + var str = "CamelCaseExample"; + Assert.AreEqual ("camelCaseExample", str.PascalCase ()); + } + + [TestCase ("@thisIsNotCapitalized", "ThisIsNotCapitalized")] + [TestCase ("thisIsNotCapitalized", "ThisIsNotCapitalized")] + [TestCase ("t", "T")] + public void CapitalizeTest (string input, string output) + => Assert.AreEqual (output, input.Capitalize ()); } }