Files
Umbraco-CMS/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs
Vitor Rodrigues d624eaff33 Added MaybeNullAttribute to ModelsBuilder generated files
The added attribute will enable consuming projects that have Nullable Reference Types enabled to receive proper warnings when access null checks aren't performed.
2021-07-13 01:26:03 +02:00

583 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building
{
/// <summary>
/// Implements a builder that works by writing text.
/// </summary>
public class TextBuilder : Builder
{
/// <summary>
/// Initializes a new instance of the <see cref="TextBuilder"/> class with a list of models to generate
/// and the result of code parsing.
/// </summary>
/// <param name="typeModels">The list of models to generate.</param>
public TextBuilder(ModelsBuilderSettings config, IList<TypeModel> typeModels)
: base(config, typeModels)
{ }
// internal for unit tests only
public TextBuilder()
{ }
/// <summary>
/// Outputs a generated model to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="typeModel">The model to generate.</param>
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");
}
/// <summary>
/// Outputs generated models to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="typeModels">The models to generate.</param>
public void Generate(StringBuilder sb, IEnumerable<TypeModel> 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");
}
/// <summary>
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
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);
}
// writes an attribute that specifies that an output may be null.
// (useful for consuming projects with nullable reference types enabled)
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false)
{
sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, isReturn ? "return: " : "");
}
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/// <summary>{0}</summary>\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/// <summary>{0}</summary>\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");
WriteMaybeNullAttribute(sb, "\t\t", true);
sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n");
sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n");
WriteGeneratedCodeAttribute(sb, "\t\t");
WriteMaybeNullAttribute(sb, "\t\t", true);
sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType<TValue>(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression<Func<{0}, TValue>> selector)\n",
type.ClrName);
sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n");
sb.Append("#pragma warning restore 0109\n\n");
sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;");
// write the ctor
sb.AppendFormat("\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\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///<summary>\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///</summary>\n");
}
WriteGeneratedCodeAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
sb.Append("\t\tpublic virtual ");
WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0} => ",
property.ClrName);
WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName);
sb.AppendFormat(".{0}(this, _publishedValueFallback);\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///<summary>\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///</summary>\n");
}
WriteGeneratedCodeAttribute(sb, "\t\t");
if (!property.ModelClrType.IsValueType)
WriteMaybeNullAttribute(sb, "\t\t");
sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);
if (mixinStatic)
{
sb.Append("\t\tpublic virtual ");
WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0} => {1}(this, _publishedValueFallback);\n",
property.ClrName, MixinStaticGetterName(property.ClrName));
}
else
{
sb.Append("\t\tpublic virtual ");
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("(_publishedValueFallback, \"{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/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
if (!property.ModelClrType.IsValueType)
WriteMaybeNullAttribute(sb, "\t\t", true);
sb.Append("\t\tpublic static ");
WriteClrType(sb, property.ClrTypeName);
sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value",
mixinStaticGetterName, mixinClrName);
if (property.ModelClrType != typeof(object))
{
sb.Append("<");
WriteClrType(sb, property.ClrTypeName);
sb.Append(">");
}
sb.AppendFormat("(publishedValueFallback, \"{0}\");\n",
property.Alias);
}
private static IEnumerable<string> 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/// <summary>{0}</summary>\n", XmlCommentString(property.Name));
WriteGeneratedCodeAttribute(sb, "\t\t");
if (!property.ModelClrType.IsValueType)
WriteMaybeNullAttribute(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
public 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(Constants.CharArrays.GreaterThan).Split(Constants.CharArrays.Comma); // 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 <T> 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<string, string> TypesMap = new Dictionary<string, string>(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" }
};
}
}