using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Core.Configuration; namespace Umbraco.ModelsBuilder.Embedded.Building { /// /// Implements a builder that works by writing text. /// internal class TextBuilder : Builder { /// /// Initializes a new instance of the class with a list of models to generate /// and the result of code parsing. /// /// The list of models to generate. public TextBuilder(IModelsBuilderConfig config, IList typeModels) : base(config, typeModels) { } // internal for unit tests only internal TextBuilder() { } /// /// Outputs a generated model to a string builder. /// /// The string builder. /// The model to generate. public void Generate(StringBuilder sb, TypeModel typeModel) { WriteHeader(sb); foreach (var t in TypesUsing) sb.AppendFormat("using {0};\n", t); sb.Append("\n"); sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); sb.Append("{\n"); WriteContentType(sb, typeModel); sb.Append("}\n"); } /// /// Outputs generated models to a string builder. /// /// The string builder. /// The models to generate. public void Generate(StringBuilder sb, IEnumerable typeModels) { WriteHeader(sb); foreach (var t in TypesUsing) sb.AppendFormat("using {0};\n", t); // assembly attributes marker sb.Append("\n//ASSATTR\n"); sb.Append("\n"); sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); sb.Append("{\n"); foreach (var typeModel in typeModels) { WriteContentType(sb, typeModel); sb.Append("\n"); } sb.Append("}\n"); } /// /// Outputs an "auto-generated" header to a string builder. /// /// The string builder. public static void WriteHeader(StringBuilder sb) { TextHeaderWriter.WriteHeader(sb); } // writes an attribute that identifies code generated by a tool // (helps reduce warnings, tools such as FxCop use it) // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ // // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. // private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) { sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); } private void WriteContentType(StringBuilder sb, TypeModel type) { string sep; if (type.IsMixin) { // write the interface declaration sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); var implements = type.BaseType == null ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) : type.BaseType.ClrName; if (implements != null) sb.AppendFormat(" : I{0}", implements); // write the mixins sep = implements == null ? ":" : ","; foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) { sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); sep = ","; } sb.Append("\n\t{\n"); // write the properties - only the local (non-ignored) ones, we're an interface var more = false; foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) { if (more) sb.Append("\n"); more = true; WriteInterfaceProperty(sb, prop); } sb.Append("\t}\n\n"); } // write the class declaration if (!string.IsNullOrWhiteSpace(type.Name)) sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); // cannot do it now. see note in ImplementContentTypeAttribute //if (!type.HasImplement) // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); sb.AppendFormat("\tpublic partial class {0}", type.ClrName); var inherits = type.HasBase ? null // has its own base already : (type.BaseType == null ? GetModelsBaseClassName(type) : type.BaseType.ClrName); if (inherits != null) sb.AppendFormat(" : {0}", inherits); sep = inherits == null ? ":" : ","; if (type.IsMixin) { // if it's a mixin it implements its own interface sb.AppendFormat("{0} I{1}", sep, type.ClrName); } else { // write the mixins, if any, as interfaces // only if not a mixin because otherwise the interface already has them already foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) { sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); sep = ","; } } // begin class body sb.Append("\n\t{\n"); // write the constants & static methods // as 'new' since parent has its own - or maybe not - disable warning sb.Append("\t\t// helpers\n"); sb.Append("#pragma warning disable 0109 // new is redundant\n"); WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", type.Alias); var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", itemType); WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType()\n"); sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(Expression> selector)\n", type.ClrName); sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); sb.Append("#pragma warning restore 0109\n\n"); // write the ctor sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", type.ClrName, type.IsElement ? "Element" : "Content"); // write the properties sb.Append("\t\t// properties\n"); WriteContentTypeProperties(sb, type); // close the class declaration sb.Append("\t}\n"); } private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) { var staticMixinGetters = true; // write the properties foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); // no need to write the parent properties since we inherit from the parent // and the parent defines its own properties. need to write the mixins properties // since the mixins are only interfaces and we have to provide an implementation. // write the mixins properties foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) if (staticMixinGetters) WriteMixinProperty(sb, prop, mixinType.ClrName); else WriteProperty(sb, mixinType, prop); } private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) { sb.Append("\n"); // Adds xml summary to each property containing // property name and property description if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) { sb.Append("\t\t///\n"); if (!string.IsNullOrWhiteSpace(property.Description)) sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); else sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); sb.Append("\t\t///\n"); } WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); sb.Append("\t\tpublic "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => ", property.ClrName); WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); sb.AppendFormat(".{0}(this);\n", MixinStaticGetterName(property.ClrName)); } private static string MixinStaticGetterName(string clrName) { return string.Format("Get{0}", clrName); } private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) { var mixinStatic = mixinClrName != null; sb.Append("\n"); if (property.Errors != null) { sb.Append("\t\t/*\n"); sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); sb.Append("\t\t *\n"); var first = true; foreach (var error in property.Errors) { if (first) first = false; else sb.Append("\t\t *\n"); foreach (var s in SplitError(error)) { sb.Append("\t\t * "); sb.Append(s); sb.Append("\n"); } } sb.Append("\t\t *\n"); sb.Append("\n"); } // Adds xml summary to each property containing // property name and property description if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) { sb.Append("\t\t///\n"); if (!string.IsNullOrWhiteSpace(property.Description)) sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); else sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); sb.Append("\t\t///\n"); } WriteGeneratedCodeAttribute(sb, "\t\t"); sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); if (mixinStatic) { sb.Append("\t\tpublic "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => {1}(this);\n", property.ClrName, MixinStaticGetterName(property.ClrName)); } else { sb.Append("\t\tpublic "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} => this.Value", property.ClrName); if (property.ModelClrType != typeof(object)) { sb.Append("<"); WriteClrType(sb, property.ClrTypeName); sb.Append(">"); } sb.AppendFormat("(\"{0}\");\n", property.Alias); } if (property.Errors != null) { sb.Append("\n"); sb.Append("\t\t *\n"); sb.Append("\t\t */\n"); } if (!mixinStatic) return; var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; sb.Append("\n"); if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\tpublic static "); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0}(I{1} that) => that.Value", mixinStaticGetterName, mixinClrName); if (property.ModelClrType != typeof(object)) { sb.Append("<"); WriteClrType(sb, property.ClrTypeName); sb.Append(">"); } sb.AppendFormat("(\"{0}\");\n", property.Alias); } private static IEnumerable SplitError(string error) { var p = 0; while (p < error.Length) { var n = p + 50; while (n < error.Length && error[n] != ' ') n++; if (n >= error.Length) break; yield return error.Substring(p, n - p); p = n + 1; } if (p < error.Length) yield return error.Substring(p); } private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) { if (property.Errors != null) { sb.Append("\t\t/*\n"); sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); sb.Append("\t\t *\n"); var first = true; foreach (var error in property.Errors) { if (first) first = false; else sb.Append("\t\t *\n"); foreach (var s in SplitError(error)) { sb.Append("\t\t * "); sb.Append(s); sb.Append("\n"); } } sb.Append("\t\t *\n"); sb.Append("\n"); } if (!string.IsNullOrWhiteSpace(property.Name)) sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); WriteGeneratedCodeAttribute(sb, "\t\t"); sb.Append("\t\t"); WriteClrType(sb, property.ClrTypeName); sb.AppendFormat(" {0} {{ get; }}\n", property.ClrName); if (property.Errors != null) { sb.Append("\n"); sb.Append("\t\t *\n"); sb.Append("\t\t */\n"); } } // internal for unit tests internal void WriteClrType(StringBuilder sb, Type type) { var s = type.ToString(); if (type.IsGenericType) { var p = s.IndexOf('`'); WriteNonGenericClrType(sb, s.Substring(0, p)); sb.Append("<"); var args = type.GetGenericArguments(); for (var i = 0; i < args.Length; i++) { if (i > 0) sb.Append(", "); WriteClrType(sb, args[i]); } sb.Append(">"); } else { WriteNonGenericClrType(sb, s); } } internal void WriteClrType(StringBuilder sb, string type) { var p = type.IndexOf('<'); if (type.Contains('<')) { WriteNonGenericClrType(sb, type.Substring(0, p)); sb.Append("<"); var args = type.Substring(p + 1).TrimEnd('>').Split(','); // fixme will NOT work with nested generic types for (var i = 0; i < args.Length; i++) { if (i > 0) sb.Append(", "); WriteClrType(sb, args[i]); } sb.Append(">"); } else { WriteNonGenericClrType(sb, type); } } private void WriteNonGenericClrType(StringBuilder sb, string s) { // map model types s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); // takes care eg of "System.Int32" vs. "int" if (TypesMap.TryGetValue(s, out string typeName)) { sb.Append(typeName); return; } // if full type name matches a using clause, strip // so if we want Umbraco.Core.Models.IPublishedContent // and using Umbraco.Core.Models, then we just need IPublishedContent typeName = s; string typeUsing = null; var p = typeName.LastIndexOf('.'); if (p > 0) { var x = typeName.Substring(0, p); if (Using.Contains(x)) { typeName = typeName.Substring(p + 1); typeUsing = x; } else if (x == ModelsNamespace) // that one is used by default { typeName = typeName.Substring(p + 1); typeUsing = ModelsNamespace; } } // nested types *after* using typeName = typeName.Replace("+", "."); // symbol to test is the first part of the name // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous p = typeName.IndexOf('.'); var symbol = p > 0 ? typeName.Substring(0, p) : typeName; // what we should find - WITHOUT any generic thing - just the type // no 'using' = the exact symbol // a 'using' = using.symbol var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); // if not ambiguous, be happy if (!IsAmbiguousSymbol(symbol, match)) { sb.Append(typeName); return; } // symbol is ambiguous // if no 'using', must prepend global:: if (typeUsing == null) { sb.Append("global::"); sb.Append(s.Replace("+", ".")); return; } // could fullname be non-ambiguous? // note: all-or-nothing, not trying to segment the using clause typeName = s.Replace("+", "."); p = typeName.IndexOf('.'); symbol = typeName.Substring(0, p); match = symbol; // still ambiguous, must prepend global:: if (IsAmbiguousSymbol(symbol, match)) sb.Append("global::"); sb.Append(typeName); } private static string XmlCommentString(string s) { return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); } private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "System.Int16", "short" }, { "System.Int32", "int" }, { "System.Int64", "long" }, { "System.String", "string" }, { "System.Object", "object" }, { "System.Boolean", "bool" }, { "System.Void", "void" }, { "System.Char", "char" }, { "System.Byte", "byte" }, { "System.UInt16", "ushort" }, { "System.UInt32", "uint" }, { "System.UInt64", "ulong" }, { "System.SByte", "sbyte" }, { "System.Single", "float" }, { "System.Double", "double" }, { "System.Decimal", "decimal" } }; } }