diff --git a/YAMLParser/KnownStuff.cs b/YAMLParser/KnownStuff.cs index 23c707f1..9bb45a4a 100644 --- a/YAMLParser/KnownStuff.cs +++ b/YAMLParser/KnownStuff.cs @@ -82,11 +82,18 @@ public static SingleType WhatItIs(MsgFile parent, string s, string extraindent) { string[] pieces = s.Split('/'); string package = null; + // sometimes, a string can contain the char '/', such as the following line: + // string CONTENT_JSON = "application/json" if (pieces.Length == 2) { + if (s.ToLower().Contains("string") && !MsgFile.resolver.ContainsKey(pieces[0])) + goto ResolvingStep; + package = pieces[0]; s = pieces[1]; } + + ResolvingStep: SingleType st = new SingleType(package, s, extraindent); parent.resolve(st); WhatItIs(parent, st); diff --git a/YAMLParser/MsgFile.cs b/YAMLParser/MsgFile.cs index e6b04212..3ab53c81 100644 --- a/YAMLParser/MsgFile.cs +++ b/YAMLParser/MsgFile.cs @@ -470,7 +470,7 @@ public override string ToString() def[i] = def[i].Replace(" ", " "); def[i] = def[i].Replace(" = ", "="); } - GUTS = GUTS.Replace("$MYMESSAGEDEFINITION", "@\"" + def.Aggregate("", (current, d) => current + (d + "\n")).Trim('\n') + "\""); + GUTS = GUTS.Replace("$MYMESSAGEDEFINITION", "@\"" + def.Aggregate("", (current, d) => current + (d + "\n")).Trim('\n').Replace("\"", "\"\"") + "\""); GUTS = GUTS.Replace("$MYHASHEADER", HasHeader.ToString().ToLower()); GUTS = GUTS.Replace("$MYFIELDS", GeneratedDictHelper.Length > 5 ? "{{" + GeneratedDictHelper + "}}" : "()"); GUTS = GUTS.Replace("$NULLCONSTBODY", ""); diff --git a/YAMLParser/MsgFileLocator.cs b/YAMLParser/MsgFileLocator.cs index 3147fdfb..172a5bc4 100644 --- a/YAMLParser/MsgFileLocator.cs +++ b/YAMLParser/MsgFileLocator.cs @@ -1,13 +1,36 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; +using FauxMessages; namespace YAMLParser { public class MsgFileLocation { + // Unicode categories according to https://stackoverflow.com/a/950651/4036588 + private static readonly UnicodeCategory[] validFirstChars = + { + UnicodeCategory.UppercaseLetter, // Lu + UnicodeCategory.LowercaseLetter, // Ll + UnicodeCategory.TitlecaseLetter, // Lt + UnicodeCategory.ModifierLetter, // Lm + UnicodeCategory.OtherLetter // Lo + // underscore is also permitted for the first char + }; + private static readonly UnicodeCategory[] otherValidChars = + { + UnicodeCategory.LetterNumber, // Nl + UnicodeCategory.NonSpacingMark, // Mn + UnicodeCategory.SpacingCombiningMark, // Mc + UnicodeCategory.DecimalDigitNumber, // Nd + UnicodeCategory.ConnectorPunctuation, // Pc + UnicodeCategory.Format // Cf + }; + private static readonly UnicodeCategory[] validCSharpChars = validFirstChars.Union(otherValidChars).ToArray(); + private static string[] MSG_GEN_FOLDER_NAMES = { "msg", @@ -29,6 +52,10 @@ private static string getPackageName(string path) string foldername = chunks[chunks.Length - 2]; if (MSG_GEN_FOLDER_NAMES.Contains(foldername)) foldername = chunks[chunks.Length - 3]; + + if (!IsValidCSharpIdentifier(foldername)) + throw new ArgumentException(String.Format("'{0}' from '{1}' is not a compatible C# identifier name\n\tThe package name must conform to C# Language Specifications (refer to this StackOverflow answer: https://stackoverflow.com/a/950651/4036588)\n", foldername, path)); + return foldername; } @@ -75,6 +102,37 @@ public override string ToString() { return string.Format("{0}.{1}", System.IO.Path.Combine(package, basename), extension); } + + public static bool IsValidCSharpIdentifier(string toTest) + { + if (toTest.Length == 0) // obviously..? + return false; + + if (SingleType.IsCSharpKeyword(toTest)) // best to avoid any complications + return false; + + char[] letters = toTest.ToCharArray(); + char first = letters[0]; + + if (first != '_' && !validFirstChars.Contains(CharUnicodeInfo.GetUnicodeCategory(first))) + return false; + + foreach (char c in letters) + if (!validCSharpChars.Contains(CharUnicodeInfo.GetUnicodeCategory(c))) + return false; + + return true; + + // TODO: fix this regex method, replace the above method with it, + // and get rid of the three UnicodeCategory arrays + // (regex method found at https://stackoverflow.com/a/1904462/4036588) + + //const string start = @"(\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl})"; + //const string extend = @"(\p{Mn}|\p{Mc}|\p{Nd}|\p{Pc}|\p{Cf})"; + //Regex ident = new Regex(string.Format("{0}({0}|{1})*", start, extend)); + //toTest = toTest.Normalize(); + //return ident.IsMatch(toTest); + } } internal static class MsgFileLocator @@ -112,6 +170,22 @@ private static void explode(List m, List s, Li Console.WriteLine("Skipped " + (msgfiles.Length - (m.Count - mb4)) + " duplicate msgs and " + (srvfiles.Length - (s.Count - sb4)) + " duplicate srvs"); } + internal static int priority(string package) + { + switch (package) + { + case "std_msgs": return 1; + case "geometry_msgs": return 2; + case "actionlib_msgs": return 3; + default: return 9; + } + } + + internal static List sortMessages(List msgs) + { + return msgs.OrderBy(m => "" + priority(m.package) + m.package + m.basename).ToList(); + } + public static void findMessages(List msgs, List srvs, List actionFiles, params string[] args) { diff --git a/YAMLParser/Program.cs b/YAMLParser/Program.cs index 7037478c..0fea2f52 100644 --- a/YAMLParser/Program.cs +++ b/YAMLParser/Program.cs @@ -21,6 +21,7 @@ internal class Program static List actionFiles = new List(); private static ILogger Logger { get; set; } const string DEFAULT_OUTPUT_FOLDERNAME = "Messages"; + static readonly string[] required_packages = {"std_msgs", "geometry_msgs", /*"actionlib_msgs",*/ "sensor_msgs"}; public static void Main(params string[] args) { @@ -45,8 +46,10 @@ public static void Main(params string[] args) return 1; } + List dirs = messageDirectories.Value().Split(',').ToList(); + Program.Run( - messageDirectories.HasValue() ? messageDirectories.Values : null, + messageDirectories.HasValue() ? dirs : null, assemblies.HasValue() ? assemblies.Values : null, outputDirectory.HasValue() ? outputDirectory.Value() : null, interactive.HasValue(), @@ -127,6 +130,8 @@ private static void Run(List messageDirs, List assemblies = null Console.WriteLine("Looking in " + d); MsgFileLocator.findMessages(paths, pathssrv, actionFileLocations, d); } + // sort paths by priority + paths = MsgFileLocator.sortMessages(paths); // first pass: create all msg files (and register them in static resolver dictionary) var baseTypes = MessageTypeRegistry.Default.GetTypeNames().ToList(); @@ -163,6 +168,14 @@ private static void Run(List messageDirs, List assemblies = null actionFiles = actionFileParser.GenerateRosMessageClasses(); //var actionFiles = new List(); + if (!StdMsgsProcessed()) // may seem obvious, but needed so that all other messages can build... + { + string resolvedPkgs = String.Join(", ", MsgFile.resolver.Keys.OrderBy(x => x.ToString()).ToArray()); + Console.WriteLine("Missing at least one of the following ROS packages: [\"" + String.Join("\", \"", required_packages) + "\"]. Exiting..."); + Console.WriteLine("resolver's keys: [" + resolvedPkgs + "]"); + return; + } + if (paths.Count + pathssrv.Count > 0) { MakeTempDir(outputdir); @@ -184,6 +197,11 @@ private static void Run(List messageDirs, List assemblies = null } } + public static bool StdMsgsProcessed() + { + return required_packages.All(c => MsgFile.resolver.ContainsKey(c)); + } + private static void MakeTempDir(string outputdir) { if (!Directory.Exists(outputdir)) diff --git a/YAMLParser/SingleType.cs b/YAMLParser/SingleType.cs index db114bf7..09c21a0d 100644 --- a/YAMLParser/SingleType.cs +++ b/YAMLParser/SingleType.cs @@ -11,10 +11,12 @@ namespace FauxMessages { public class SingleType { - // TODO extend check to other C# keywords - private static readonly string[] CSharpKeywords = { "object", "params", "namespace", "const", "static" }; + // c# keywords listed on https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ as of August 1st, 2018 + private static readonly string[] reserved_keywords = { "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "using static", "virtual", "void", "volatile", "while" }; + private static readonly string[] contextual_keywords = { "add", "alias", "ascending", "async", "await", "descending", "dynamic", "from", "get", "global", "group", "into", "join", "let", "nameof", "orderby", "partial", "partial", "remove", "select", "set", "value", "var", "when", "where", "where", "yield" }; + private static readonly string[] CSharpKeywords = reserved_keywords.Union(contextual_keywords).ToArray(); - private static bool IsCSharpKeyword(string name) + public static bool IsCSharpKeyword(string name) { return CSharpKeywords.Contains(name); } @@ -107,9 +109,16 @@ public void Finalize(MsgFile parent, string[] s, bool isliteral) otherstuff = " = " + parts[1]; } - if (IsCSharpKeyword(name)) + if (name == parent.Name.Split(".").Last() || !MsgFileLocation.IsValidCSharpIdentifier(name) && name.Length > 0) { - name = "@" + name; + if (IsCSharpKeyword(name)) + { + name = "@" + name; + } + else if (MsgFileLocation.IsValidCSharpIdentifier(name) && name == parent.Name.Split(".").Last()) + name = "_" + name; + else + throw new ArgumentException(String.Format("Variable '{0}' from '{1}' is not a compatible C# identifier name\n\tAll variable names must conform to C# Language Specifications (refer to this StackOverflow answer: https://stackoverflow.com/a/950651/4036588)\n", name, parent.msgFileLocation.Path)); } for (int i = 2; i < s.Length; i++) @@ -151,14 +160,16 @@ public void Finalize(MsgFile parent, string[] s, bool isliteral) string prefix = "", suffix = ""; if (isconst) { - if (!type.Equals("string", StringComparison.OrdinalIgnoreCase)) - { + // why can't strings be constants? + + //if (!type.Equals("string", StringComparison.OrdinalIgnoreCase)) + //{ if (KnownStuff.IsPrimitiveType(this)) prefix = "const "; else prefix = "static readonly "; wantsconstructor = false; - } + //} } string t = KnownStuff.GetNamespacedType(this, type); @@ -225,10 +236,19 @@ public void refinalize(MsgFile parent, string REALTYPE) name = parts[0]; otherstuff = " = " + parts[1]; } - if (IsCSharpKeyword(name)) + + if (name == parent.Name.Split(".").Last() || !MsgFileLocation.IsValidCSharpIdentifier(name) && name.Length > 0) { - name = "@" + name; + if (IsCSharpKeyword(name)) + { + name = "@" + name; + } + else if (MsgFileLocation.IsValidCSharpIdentifier(name) && name == parent.Name.Split(".").Last()) + name = "_" + name; + else + throw new ArgumentException(String.Format("Variable '{0}' from '{1}' is not a compatible C# identifier name\n\tAll variable names must conform to C# Language Specifications (refer to this StackOverflow answer: https://stackoverflow.com/a/950651/4036588)\n", name, parent.msgFileLocation.Path)); } + for (int i = 2; i < backup.Length; i++) otherstuff += " " + backup[i]; if (otherstuff.Contains('=')) @@ -269,10 +289,12 @@ public void refinalize(MsgFile parent, string REALTYPE) string prefix = "", suffix = ""; if (isconst) { - if (!Type.Equals("string", StringComparison.OrdinalIgnoreCase)) - { + // why can't strings be constants? + + //if (!Type.Equals("string", StringComparison.OrdinalIgnoreCase)) + //{ prefix = "const "; - } + //} } if (otherstuff.Contains('=')) if (wantsconstructor)