From e3cd90f63e600d271d5c7b4b7c879ccb1ebf483a Mon Sep 17 00:00:00 2001 From: Tom Glastonbury <1101693+tg73@users.noreply.github.com> Date: Wed, 10 Mar 2021 14:55:16 +0000 Subject: [PATCH] Allow custom fields to be added to the generated ThisAssembly class. --- .../AssemblyInfoTest.cs | 65 +++- .../AssemblyVersionInfo.cs | 320 ++++++++++++------ .../build/Nerdbank.GitVersioning.targets | 1 + 3 files changed, 275 insertions(+), 111 deletions(-) diff --git a/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs b/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs index 4137da6b..f8fc5fb2 100644 --- a/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs +++ b/src/NerdBank.GitVersioning.Tests/AssemblyInfoTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Build.Utilities; using Nerdbank.GitVersioning.Tasks; using Xunit; @@ -18,6 +19,25 @@ public void FSharpGenerator(bool? thisAssemblyClass) info.AssemblyCompany = "company"; info.AssemblyFileVersion = "1.3.1.0"; info.AssemblyVersion = "1.3.0.0"; + info.AdditionalThisAssemblyFields = + new TaskItem[] + { + new TaskItem( + "CustomString1", + new Dictionary() { { "String", "abc" } } ), + new TaskItem( + "CustomString2", + new Dictionary() { { "String", "" } } ), + new TaskItem( + "CustomString3", + new Dictionary() { { "String", "" }, { "EmitIfEmpty", "true" } } ), + new TaskItem( + "CustomBool", + new Dictionary() { { "Boolean", "true" } } ), + new TaskItem( + "CustomTicks", + new Dictionary() { { "Ticks", "637509805729817056" } } ), + }; info.CodeLanguage = "f#"; if (thisAssemblyClass.HasValue) @@ -49,11 +69,15 @@ namespace AssemblyInfo [] #endif type internal ThisAssembly() = - static member internal AssemblyVersion = ""1.3.0.0"" - static member internal AssemblyFileVersion = ""1.3.1.0"" static member internal AssemblyCompany = ""company"" - static member internal IsPublicRelease = false + static member internal AssemblyFileVersion = ""1.3.1.0"" + static member internal AssemblyVersion = ""1.3.0.0"" + static member internal CustomBool = true + static member internal CustomString1 = ""abc"" + static member internal CustomString3 = """" + static member internal CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc) static member internal IsPrerelease = false + static member internal IsPublicRelease = false static member internal RootNamespace = """" do() " : "")}"; @@ -72,6 +96,25 @@ public void CSharpGenerator(bool? thisAssemblyClass) info.AssemblyFileVersion = "1.3.1.0"; info.AssemblyVersion = "1.3.0.0"; info.CodeLanguage = "c#"; + info.AdditionalThisAssemblyFields = + new TaskItem[] + { + new TaskItem( + "CustomString1", + new Dictionary() { { "String", "abc" } } ), + new TaskItem( + "CustomString2", + new Dictionary() { { "String", "" } } ), + new TaskItem( + "CustomString3", + new Dictionary() { { "String", "" }, { "EmitIfEmpty", "true" } } ), + new TaskItem( + "CustomBool", + new Dictionary() { { "Boolean", "true" } } ), + new TaskItem( + "CustomTicks", + new Dictionary() { { "Ticks", "637509805729817056" } } ), + }; if (thisAssemblyClass.HasValue) { @@ -100,11 +143,15 @@ public void CSharpGenerator(bool? thisAssemblyClass) [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] #endif internal static partial class ThisAssembly {{ - internal const string AssemblyVersion = ""1.3.0.0""; - internal const string AssemblyFileVersion = ""1.3.1.0""; internal const string AssemblyCompany = ""company""; - internal const bool IsPublicRelease = false; + internal const string AssemblyFileVersion = ""1.3.1.0""; + internal const string AssemblyVersion = ""1.3.0.0""; + internal const bool CustomBool = true; + internal const string CustomString1 = ""abc""; + internal const string CustomString3 = """"; + internal static readonly System.DateTime CustomTicks = new System.DateTime(637509805729817056L, System.DateTimeKind.Utc); internal const bool IsPrerelease = false; + internal const bool IsPublicRelease = false; internal const string RootNamespace = """"; }} " : "")}"; @@ -154,11 +201,11 @@ public void VisualBasicGenerator(bool? thisAssemblyClass) #Else Partial Friend NotInheritable Class ThisAssembly #End If - Friend Const AssemblyVersion As String = ""1.3.0.0"" - Friend Const AssemblyFileVersion As String = ""1.3.1.0"" Friend Const AssemblyCompany As String = ""company"" - Friend Const IsPublicRelease As Boolean = False + Friend Const AssemblyFileVersion As String = ""1.3.1.0"" + Friend Const AssemblyVersion As String = ""1.3.0.0"" Friend Const IsPrerelease As Boolean = False + Friend Const IsPublicRelease As Boolean = False Friend Const RootNamespace As String = """" End Class " : "")}"; diff --git a/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs b/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs index 8ad58eba..8d869850 100644 --- a/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs +++ b/src/Nerdbank.GitVersioning.Tasks/AssemblyVersionInfo.cs @@ -88,6 +88,25 @@ public class AssemblyVersionInfo : Task public bool EmitThisAssemblyClass { get; set; } = true; + /// + /// Specify additional fields to be added to the ThisAssembly class. + /// + /// + /// Field name is given by %(Identity). Provide the field value by specifying exactly one metadata value that is %(String), %(Boolean) or %(Ticks) (for UTC DateTime). + /// If specifying %(String), you can also specify %(EmitIfEmpty) to determine if a class member is added even if the value is an empty string (default is false). + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + public ITaskItem[] AdditionalThisAssemblyFields { get; set; } + #if NET461 public override bool Execute() { @@ -154,46 +173,31 @@ private CodeTypeDeclaration CreateThisAssemblyClass() // CodeDOM doesn't support static classes, so hide the constructor instead. thisAssembly.Members.Add(new CodeConstructor { Attributes = MemberAttributes.Private }); - // Determine information about the public key used in the assembly name. - string publicKey, publicKeyToken; - bool hasKeyInfo = this.TryReadKeyInfo(out publicKey, out publicKeyToken); + var fields = this.GetFieldsForThisAssembly(); - // Define the constants. - thisAssembly.Members.AddRange(CreateFields(new Dictionary - { - { "AssemblyVersion", this.AssemblyVersion }, - { "AssemblyFileVersion", this.AssemblyFileVersion }, - { "AssemblyInformationalVersion", this.AssemblyInformationalVersion }, - { "AssemblyName", this.AssemblyName }, - { "AssemblyTitle", this.AssemblyTitle }, - { "AssemblyProduct", this.AssemblyProduct }, - { "AssemblyCopyright", this.AssemblyCopyright }, - { "AssemblyCompany", this.AssemblyCompany }, - { "AssemblyConfiguration", this.AssemblyConfiguration }, - { "GitCommitId", this.GitCommitId }, - }).ToArray()); - thisAssembly.Members.AddRange(CreateFields(new Dictionary + foreach (var pair in fields) + { + switch (pair.Value.Value) { - { "IsPublicRelease", this.PublicRelease }, - { "IsPrerelease", !string.IsNullOrEmpty(this.PrereleaseVersion) }, - }).ToArray()); + case string stringValue: + if (pair.Value.EmitIfEmpty || !string.IsNullOrEmpty(stringValue)) + { + thisAssembly.Members.Add(CreateField(pair.Key, stringValue)); + } + break; - if (long.TryParse(this.GitCommitDateTicks, out long gitCommitDateTicks)) - { - thisAssembly.Members.AddRange(CreateCommitDateProperty(gitCommitDateTicks).ToArray()); - } + case bool boolValue: + thisAssembly.Members.Add(CreateField(pair.Key, boolValue)); + break; - if (hasKeyInfo) - { - thisAssembly.Members.AddRange(CreateFields(new Dictionary - { - { "PublicKey", publicKey }, - { "PublicKeyToken", publicKeyToken }, - }).ToArray()); - } + case long ticksValue: + thisAssembly.Members.AddRange(CreateField(pair.Key, ticksValue).ToArray()); + break; - // These properties should be defined even if they are empty. - thisAssembly.Members.Add(CreateField("RootNamespace", this.RootNamespace)); + default: + throw new NotImplementedException(); + } + } return thisAssembly; } @@ -227,25 +231,6 @@ private IEnumerable CreateAssemblyAttributes() } } - private static IEnumerable CreateFields(IReadOnlyDictionary namesAndValues) - { - foreach (var item in namesAndValues) - { - if (!string.IsNullOrEmpty(item.Value)) - { - yield return CreateField(item.Key, item.Value); - } - } - } - - private static IEnumerable CreateFields(IReadOnlyDictionary namesAndValues) - { - foreach (var item in namesAndValues) - { - yield return CreateField(item.Key, item.Value); - } - } - private static CodeMemberField CreateField(string name, T value) { return new CodeMemberField(typeof(T), name) @@ -255,10 +240,32 @@ private static CodeMemberField CreateField(string name, T value) }; } - private static IEnumerable CreateCommitDateProperty(long ticks) + private static IEnumerable CreateField(string name, long ticks) { + if ( string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + // internal static System.DateTime GitCommitDate {{ get; }} = new System.DateTime({ticks}, System.DateTimeKind.Utc);"); - yield return new CodeMemberField(typeof(DateTime), "gitCommitDate") + + // For backing field name, try to use name with first char converted to lower case, or otherwise suffix with underscore. + string fieldName = null; + var char0 = name[0]; + + if ( char.IsUpper( char0) ) + { + fieldName = + name.Length == 1 + ? new string(char.ToLowerInvariant(char0), 1) + : new string(char.ToLowerInvariant(char0), 1) + name.Substring(1); + } + else + { + fieldName = name + "_"; + } + + yield return new CodeMemberField(typeof(DateTime), fieldName) { Attributes = MemberAttributes.Private, InitExpression = new CodeObjectCreateExpression( @@ -273,7 +280,7 @@ private static IEnumerable CreateCommitDateProperty(long ticks) { Attributes = MemberAttributes.Assembly, Type = new CodeTypeReference(typeof(DateTime)), - Name = "GitCommitDate", + Name = name, HasGet = true, HasSet = false, }; @@ -282,7 +289,7 @@ private static IEnumerable CreateCommitDateProperty(long ticks) new CodeMethodReturnStatement( new CodeFieldReferenceExpression( null, - "gitCommitDate"))); + fieldName))); yield return property; } @@ -370,60 +377,169 @@ private void GenerateAssemblyAttributes() } } - private void GenerateThisAssemblyClass() + private List> GetFieldsForThisAssembly() { - this.generator.StartThisAssemblyClass(); - // Determine information about the public key used in the assembly name. string publicKey, publicKeyToken; bool hasKeyInfo = this.TryReadKeyInfo(out publicKey, out publicKeyToken); // Define the constants. - var fields = new Dictionary - { - { "AssemblyVersion", this.AssemblyVersion }, - { "AssemblyFileVersion", this.AssemblyFileVersion }, - { "AssemblyInformationalVersion", this.AssemblyInformationalVersion }, - { "AssemblyName", this.AssemblyName }, - { "AssemblyTitle", this.AssemblyTitle }, - { "AssemblyProduct", this.AssemblyProduct }, - { "AssemblyCopyright", this.AssemblyCopyright }, - { "AssemblyCompany", this.AssemblyCompany }, - { "AssemblyConfiguration", this.AssemblyConfiguration }, - { "GitCommitId", this.GitCommitId }, - }; - var boolFields = new Dictionary + var fields = new Dictionary { - { "IsPublicRelease", this.PublicRelease }, - { "IsPrerelease", !string.IsNullOrEmpty(this.PrereleaseVersion) }, + { "AssemblyVersion", (this.AssemblyVersion, false) }, + { "AssemblyFileVersion", (this.AssemblyFileVersion, false) }, + { "AssemblyInformationalVersion", (this.AssemblyInformationalVersion, false) }, + { "AssemblyName", (this.AssemblyName, false) }, + { "AssemblyTitle", (this.AssemblyTitle, false) }, + { "AssemblyProduct", (this.AssemblyProduct, false) }, + { "AssemblyCopyright", (this.AssemblyCopyright, false) }, + { "AssemblyCompany", (this.AssemblyCompany, false) }, + { "AssemblyConfiguration", (this.AssemblyConfiguration, false) }, + { "GitCommitId", (this.GitCommitId, false) }, + // These properties should be defined even if they are empty strings: + { "RootNamespace", (this.RootNamespace, true) }, + // These non-string properties are always emitted: + { "IsPublicRelease", (this.PublicRelease, true) }, + { "IsPrerelease", (!string.IsNullOrEmpty(this.PrereleaseVersion), true) }, }; if (hasKeyInfo) { - fields.Add("PublicKey", publicKey); - fields.Add("PublicKeyToken", publicKeyToken); + fields.Add("PublicKey", (publicKey, false)); + fields.Add("PublicKeyToken", (publicKeyToken, false)); } - foreach (var pair in fields) + if (long.TryParse(this.GitCommitDateTicks, out long gitCommitDateTicks)) { - if (!string.IsNullOrEmpty(pair.Value)) - { - this.generator.AddThisAssemblyMember(pair.Key, pair.Value); - } + fields.Add("GitCommitDate", (gitCommitDateTicks, true)); } - foreach (var pair in boolFields) + if (this.AdditionalThisAssemblyFields != null && this.AdditionalThisAssemblyFields.Length > 0) { - this.generator.AddThisAssemblyMember(pair.Key, pair.Value); + foreach (var item in this.AdditionalThisAssemblyFields) + { + if (item == null) + continue; + + var name = item.ItemSpec.Trim(); + var metaClone = item.CloneCustomMetadata(); + var meta = new Dictionary(metaClone.Count, StringComparer.OrdinalIgnoreCase); + var iter = metaClone.GetEnumerator(); + + while ( iter.MoveNext() ) + { + meta.Add((string)iter.Key, (string)iter.Value); + } + + object value = null; + bool emitIfEmpty = false; + + if (meta.TryGetValue("String", out var stringValue)) + { + value = stringValue; + if (meta.TryGetValue("EmitIfEmpty", out var emitIfEmptyString)) + { + if (!bool.TryParse(emitIfEmptyString, out emitIfEmpty)) + { + this.Log.LogError("The value '{0}' for EmitIfEmpty metadata for item '{1}' in AdditionalThisAssemblyFields is not valid.", emitIfEmptyString, name); + continue; + } + } + } + + if (meta.TryGetValue("Boolean", out var boolText)) + { + if (value != null) + { + this.Log.LogError("The metadata for item '{0}' in AdditionalThisAssemblyFields specifies more than one kind of value.", name); + continue; + } + + if (bool.TryParse(boolText, out var boolValue)) + { + value = boolValue; + } + else + { + this.Log.LogError("The Boolean value '{0}' for item '{1}' in AdditionalThisAssemblyFields is not valid.", boolText, name); + continue; + } + } + + if (meta.TryGetValue("Ticks", out var ticksText)) + { + if (value != null) + { + this.Log.LogError("The metadata for item '{0}' in AdditionalThisAssemblyFields specifies more than one kind of value.", name); + continue; + } + + if (long.TryParse(ticksText, out var ticksValue)) + { + value = ticksValue; + } + else + { + this.Log.LogError("The Ticks value '{0}' for item '{1}' in AdditionalThisAssemblyFields is not valid.", ticksText, name); + continue; + } + } + + if ( value == null ) + { + this.Log.LogWarning("Field '{0}' in AdditionalThisAssemblyFields has no value and will be ignored.", name); + continue; + } + + if (fields.ContainsKey(name)) + { + this.Log.LogError("Field name '{0}' in AdditionalThisAssemblyFields has already been defined.", name); + continue; + } + + fields.Add(name, (value, emitIfEmpty)); + } } - if (long.TryParse(this.GitCommitDateTicks, out long gitCommitDateTicks)) + return fields.OrderBy(f => f.Key).ToList(); + } + + private void GenerateThisAssemblyClass() + { + this.generator.StartThisAssemblyClass(); + + var fields = this.GetFieldsForThisAssembly(); + + foreach (var pair in fields) { - this.generator.AddCommitDateProperty(gitCommitDateTicks); - } + switch (pair.Value.Value) + { + case null: + if (pair.Value.EmitIfEmpty) + { + this.generator.AddThisAssemblyMember(pair.Key, string.Empty); + } + break; - // These properties should be defined even if they are empty. - this.generator.AddThisAssemblyMember("RootNamespace", this.RootNamespace); + case string stringValue: + if (pair.Value.EmitIfEmpty || !string.IsNullOrEmpty(stringValue)) + { + this.generator.AddThisAssemblyMember(pair.Key, stringValue); + } + break; + + case bool boolValue: + this.generator.AddThisAssemblyMember(pair.Key, boolValue); + break; + + case long ticksValue: + this.generator.AddThisAssemblyMember(pair.Key, ticksValue); + break; + + default: + throw new NotImplementedException(); + } + } this.generator.EndThisAssemblyClass(); } @@ -464,6 +580,8 @@ internal CodeGenerator() internal abstract void AddThisAssemblyMember(string name, bool value); + internal abstract void AddThisAssemblyMember(string name, long ticks); + internal abstract void EndThisAssemblyClass(); /// @@ -479,8 +597,6 @@ internal void AddBlankLine() this.codeBuilder.AppendLine(); } - internal abstract void AddCommitDateProperty(long ticks); - protected void AddCodeComment(string comment, string token) { var sr = new StringReader(comment); @@ -510,6 +626,11 @@ internal override void AddThisAssemblyMember(string name, bool value) this.codeBuilder.AppendLine($" static member internal {name} = {(value ? "true" : "false")}"); } + internal override void AddThisAssemblyMember(string name, long ticks) + { + this.codeBuilder.AppendLine($" static member internal {name} = new System.DateTime({ticks}L, System.DateTimeKind.Utc)"); + } + internal override void EmitNamespaceIfRequired(string ns) { this.codeBuilder.AppendLine($"namespace {ns}"); @@ -520,11 +641,6 @@ internal override void DeclareAttribute(Type type, string arg) this.codeBuilder.AppendLine($"[]"); } - internal override void AddCommitDateProperty(long ticks) - { - this.codeBuilder.AppendLine($" static member internal GitCommitDate = new System.DateTime({ticks}L, System.DateTimeKind.Utc)"); - } - internal override void EndThisAssemblyClass() { this.codeBuilder.AppendLine("do()"); @@ -576,9 +692,9 @@ internal override void AddThisAssemblyMember(string name, bool value) this.codeBuilder.AppendLine($" internal const bool {name} = {(value ? "true" : "false")};"); } - internal override void AddCommitDateProperty(long ticks) + internal override void AddThisAssemblyMember(string name, long ticks) { - this.codeBuilder.AppendLine($" internal static readonly System.DateTime GitCommitDate = new System.DateTime({ticks}L, System.DateTimeKind.Utc);"); + this.codeBuilder.AppendLine($" internal static readonly System.DateTime {name} = new System.DateTime({ticks}L, System.DateTimeKind.Utc);"); } internal override void EndThisAssemblyClass() @@ -623,9 +739,9 @@ internal override void AddThisAssemblyMember(string name, bool value) this.codeBuilder.AppendLine($" Friend Const {name} As Boolean = {(value ? "True" : "False")}"); } - internal override void AddCommitDateProperty(long ticks) + internal override void AddThisAssemblyMember(string name, long ticks) { - this.codeBuilder.AppendLine($" Friend Shared ReadOnly GitCommitDate As System.DateTime = New System.DateTime({ticks}L, System.DateTimeKind.Utc)"); + this.codeBuilder.AppendLine($" Friend Shared ReadOnly {name} As System.DateTime = New System.DateTime({ticks}L, System.DateTimeKind.Utc)"); } internal override void EndThisAssemblyClass() diff --git a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets index c4a60e56..9e39ea91 100644 --- a/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets +++ b/src/Nerdbank.GitVersioning.Tasks/build/Nerdbank.GitVersioning.targets @@ -163,6 +163,7 @@ GitCommitDateTicks="$(GitCommitDateTicks)" EmitNonVersionCustomAttributes="$(NBGV_EmitNonVersionCustomAttributes)" EmitThisAssemblyClass="$(NBGV_EmitThisAssemblyClass)" + AdditionalThisAssemblyFields="@(AdditionalThisAssemblyFields)" />