From 7e299c93a8ac6f31603876053c12da1bb420c385 Mon Sep 17 00:00:00 2001 From: dsm Date: Wed, 13 Jan 2016 20:28:20 +0100 Subject: [PATCH 1/2] AssemblyInfoFile: add functions to read and update attributes --- src/app/FakeLib/AssemblyInfoFile.fs | 97 ++++++++++ src/test/Test.FAKECore/AssemblyInfoSpecs.cs | 195 ++++++++++++++++++++ 2 files changed, 292 insertions(+) diff --git a/src/app/FakeLib/AssemblyInfoFile.fs b/src/app/FakeLib/AssemblyInfoFile.fs index b863c90575e..bb6add5a430 100644 --- a/src/app/FakeLib/AssemblyInfoFile.fs +++ b/src/app/FakeLib/AssemblyInfoFile.fs @@ -2,8 +2,39 @@ /// There is also a tutorial about the [AssemblyInfo tasks](../assemblyinfo.html) available. module Fake.AssemblyInfoFile +open System +open System.IO +open System.Text.RegularExpressions + let internal assemblyVersionRegex = getRegEx @"([0-9]+.)+[0-9]+" +// matches [assembly: name(value)] and captures "name" and "value" as named captures. Variations for C#, F#, C++ and VB +let private regexAttrNameValueCs = @"^\s*\[\s*assembly:\s*(?\w+?)\s*\((?.*)\)\s*\]\s*$" +let private regexAttrNameValueFs = @"^\s*\[\<\s*assembly:\s*(?\w+?)\s*\((?.*)\)\s*\>\]\s*$" +let private regexAttrNameValueCpp = @"^\s*\[\s*assembly:\s*(?\w+?)\s*\((?.*)\)\s*\]\s*;\s*$" +let private regexAttrNameValueVb = @"^\s*\<\s*assembly:\s*(?\w+?)\s*\((?.*)\)\s*\>\s*$" + +// matches [assembly: name(value)] but only captures the value. Variations for C#, F#, C++ and VB +let private regexAttrValueCs name = + @"(?<=^\s*\[\s*assembly:\s*" + name + @"(?:Attribute)?\s*\()" // look-behind for "[assembly: name[Attribute](" + + @"(.*)" // value + + @"(?=\)\s*\]\s*$)" // look-ahead for ")]" + +let private regexAttrValueFs name = + @"(?<=^\s*\[\<\s*assembly:\s*" + name + @"(?:Attribute)?\s*\()" // look-behind for "[\]\s*$)" // look-ahead for ")>]" + +let private regexAttrValueCpp name = + @"(?<=^\s*\[\s*assembly:\s*" + name + @"(?:Attribute)?\s*\()" // look-behind for "[assembly: name[Attribute](" + + @"(.*)" // value + + @"(?=\)\s*\]\s*;\s*$)" // look-ahead for ")];" + +let private regexAttrValueVb name = + @"(?<=^\s*\<\s*assembly:\s*" + name + @"(?:Attribute)?\s*\()" // look-behind for "\s*$)" // look-ahead for ")>" + let private NormalizeVersion version = let m = assemblyVersionRegex.Match(version) if m.Captures.Count > 0 then m.Captures.[0].Value @@ -231,3 +262,69 @@ let CreateVisualBasicAssemblyInfo outputFileName attributes = /// Creates a C++/CLI AssemblyInfo file with the given attributes. let CreateCppCliAssemblyInfo outputFileName attributes = CreateCppCliAssemblyInfoWithConfig outputFileName attributes AssemblyInfoFileConfig.Default + +let private removeAtEnd (textToRemove:string) (text:string) = + if text.EndsWith(textToRemove) then + text.Substring(0, text.Length - textToRemove.Length) + else + text + +let GetAttributes assemblyInfoFile = + let text = File.ReadAllText assemblyInfoFile + + let regex = + if assemblyInfoFile.ToLower().EndsWith(".cs") then regexAttrNameValueCs + elif assemblyInfoFile.ToLower().EndsWith(".fs") then regexAttrNameValueFs + elif assemblyInfoFile.ToLower().EndsWith(".vb") then regexAttrNameValueVb + elif assemblyInfoFile.ToLower().EndsWith(".cpp") then regexAttrNameValueCpp + else + failwithf "Assembly info file type not supported: %s" assemblyInfoFile + + Regex.Matches(text, regex, RegexOptions.Multiline) + |> Seq.cast + |> Seq.map (fun m -> Attribute(m.Groups.["name"].Value |> removeAtEnd "Attribute", + m.Groups.["value"].Value, + "")) + +let GetAttribute attrName assemblyInfoFile = + assemblyInfoFile |> GetAttributes |> Seq.tryFind (fun a -> a.Name = attrName) + +let GetAttributeValue attrName assemblyInfoFile = + match GetAttribute attrName assemblyInfoFile with + | Some attr -> Some attr.Value + | None -> None + +let private updateAttr regexFactory text (attribute:Attribute) = + let regex = regexFactory attribute.Name + + let m = Regex.Match(text, regex, RegexOptions.Multiline) + + // Replace if found with different value + if m.Success && m.Value <> attribute.Value then + tracefn "Attribute '%s' updated: %s" attribute.Name attribute.Value + Regex.Replace(text, regex, attribute.Value, RegexOptions.Multiline) + + // Do nothing if found with the same value + elif m.Success then + tracefn "Attribute '%s' is already correct: %s" attribute.Name attribute.Value + text + + // Fail if not found + else + failwithf "Attribute '%s' not found" attribute.Name + +let UpdateAttributes assemblyInfoFile (attributes: seq) = + tracefn "Updating attributes in: %s" assemblyInfoFile + + let regexFactory = + if assemblyInfoFile.ToLower().EndsWith(".cs") then regexAttrValueCs + elif assemblyInfoFile.ToLower().EndsWith(".fs") then regexAttrValueFs + elif assemblyInfoFile.ToLower().EndsWith(".vb") then regexAttrValueVb + elif assemblyInfoFile.ToLower().EndsWith(".cpp") then regexAttrValueCpp + else + failwithf "Assembly info file type not supported: %s" assemblyInfoFile + + let text = File.ReadAllText assemblyInfoFile + let newText = attributes |> Seq.fold (updateAttr regexFactory) text + + File.WriteAllText(assemblyInfoFile, newText) diff --git a/src/test/Test.FAKECore/AssemblyInfoSpecs.cs b/src/test/Test.FAKECore/AssemblyInfoSpecs.cs index 39e1adb52a5..048954b22db 100644 --- a/src/test/Test.FAKECore/AssemblyInfoSpecs.cs +++ b/src/test/Test.FAKECore/AssemblyInfoSpecs.cs @@ -29,6 +29,50 @@ public class when_using_fsharp_task_with_default_config File.ReadAllText(infoFile) .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); }; + + It update_attributes_should_update_attributes_in_fs_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".fs"); + const string original = "namespace System\r\nopen System.Reflection\r\n\r\n" + + "[]\r\n" + + "[]\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var attributes = new[] + { + AssemblyInfoFile.Attribute.Product("TestLibNew"), + AssemblyInfoFile.Attribute.Version("2.0.0.0") + }; + AssemblyInfoFile.UpdateAttributes(infoFile, attributes); + + // Assert + const string expected = "namespace System\r\nopen System.Reflection\r\n\r\n" + + "[]\r\n" + + "[]\r\n"; + + File.ReadAllText(infoFile) + .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); + }; + + It get_attribute_should_read_attribute_from_fs_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".fs"); + const string original = "namespace System\r\nopen System.Reflection\r\n\r\n" + + "[]\r\n" + + "[]\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var productAttr = AssemblyInfoFile.GetAttribute("AssemblyProduct", infoFile).Value; + var versionAttr = AssemblyInfoFile.GetAttribute("AssemblyVersion", infoFile).Value; + + // Assert + productAttr.Value.ShouldEqual("\"TestLib\""); + versionAttr.Value.ShouldEqual("\"1.0.0.0\""); + }; } public class when_using_fsharp_task_with_custom_config @@ -65,6 +109,50 @@ public class when_using_csharp_task_with_default_config File.ReadAllText(infoFile) .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); }; + + It update_attributes_should_update_attributes_in_cs_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".cs"); + const string original = "// \r\nusing System.Reflection;\r\n\r\n" + + "[assembly: AssemblyProduct(\"TestLib\")]\r\n" + + "[assembly: AssemblyVersionAttribute(\"1.0.0.0\")]\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var attributes = new[] + { + AssemblyInfoFile.Attribute.Product("TestLibNew"), + AssemblyInfoFile.Attribute.Version("2.0.0.0") + }; + AssemblyInfoFile.UpdateAttributes(infoFile, attributes); + + // Assert + const string expected = "// \r\nusing System.Reflection;\r\n\r\n" + + "[assembly: AssemblyProduct(\"TestLibNew\")]\r\n" + + "[assembly: AssemblyVersionAttribute(\"2.0.0.0\")]\r\n"; + + File.ReadAllText(infoFile) + .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); + }; + + It get_attribute_should_read_attribute_from_cs_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".cs"); + const string original = "// \r\nusing System.Reflection;\r\n\r\n" + + "[assembly: AssemblyProduct(\"TestLib\")]\r\n" + + "[assembly: AssemblyVersionAttribute(\"1.0.0.0\")]\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var productAttr = AssemblyInfoFile.GetAttribute("AssemblyProduct", infoFile).Value; + var versionAttr = AssemblyInfoFile.GetAttribute("AssemblyVersion", infoFile).Value; + + // Assert + productAttr.Value.ShouldEqual("\"TestLib\""); + versionAttr.Value.ShouldEqual("\"1.0.0.0\""); + }; } @@ -84,5 +172,112 @@ public class when_using_cppcli_task_with_default_config File.ReadAllText(infoFile) .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); }; + + It update_attributes_should_update_attributes_in_cpp_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".cpp"); + const string original = "// \r\nusing namespace System::Reflection;\r\n\r\n" + + "[assembly:AssemblyProduct(\"TestLib\")];\r\n" + + "[assembly:AssemblyVersionAttribute(\"1.0.0.0\")];\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var attributes = new[] + { + AssemblyInfoFile.Attribute.Product("TestLibNew"), + AssemblyInfoFile.Attribute.Version("2.0.0.0") + }; + AssemblyInfoFile.UpdateAttributes(infoFile, attributes); + + // Assert + const string expected = "// \r\nusing namespace System::Reflection;\r\n\r\n" + + "[assembly:AssemblyProduct(\"TestLibNew\")];\r\n" + + "[assembly:AssemblyVersionAttribute(\"2.0.0.0\")];\r\n"; + + File.ReadAllText(infoFile) + .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); + }; + + It get_attribute_should_read_attribute_from_cpp_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".cpp"); + const string original = "// \r\nusing namespace System::Reflection;\r\n\r\n" + + "[assembly:AssemblyProduct(\"TestLib\")];\r\n" + + "[assembly:AssemblyVersionAttribute(\"1.0.0.0\")];\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var productAttr = AssemblyInfoFile.GetAttribute("AssemblyProduct", infoFile).Value; + var versionAttr = AssemblyInfoFile.GetAttribute("AssemblyVersion", infoFile).Value; + + // Assert + productAttr.Value.ShouldEqual("\"TestLib\""); + versionAttr.Value.ShouldEqual("\"1.0.0.0\""); + }; + } + + + public class when_using_vb_task_with_default_config + { + It should_emit_valid_syntax = () => + { + string infoFile = Path.GetTempFileName(); + var attributes = new[] + { + AssemblyInfoFile.Attribute.Product("TestLib"), + AssemblyInfoFile.Attribute.Version("1.0.0.0") + }; + AssemblyInfoFile.CreateVisualBasicAssemblyInfo(infoFile, attributes); + const string expected = "' \r\nImports System.Reflection\r\n\r\n\r\n\r\nFriend NotInheritable Class AssemblyVersionInformation\r\n Friend Const Version As String = \"1.0.0.0\"\r\nEnd Class\r\n"; + + File.ReadAllText(infoFile) + .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); + }; + + It update_attributes_should_update_attributes_in_vb_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".vb"); + const string original = "' \r\nImports System.Reflection\r\n\r\n" + + "\r\n" + + "\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var attributes = new[] + { + AssemblyInfoFile.Attribute.Product("TestLibNew"), + AssemblyInfoFile.Attribute.Version("2.0.0.0") + }; + AssemblyInfoFile.UpdateAttributes(infoFile, attributes); + + // Assert + const string expected = "' \r\nImports System.Reflection\r\n\r\n" + + "\r\n" + + "\r\n"; + + File.ReadAllText(infoFile) + .ShouldEqual(expected.Replace("\r\n", Environment.NewLine)); + }; + + It get_attribute_should_read_attribute_from_vb_file = () => + { + // Arrange. Create attribute both with and without "Attribute" at the end + string infoFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".vb"); + const string original = "' \r\nImports System.Reflection\r\n\r\n" + + "\r\n" + + "\r\n"; + File.WriteAllText(infoFile, original.Replace("\r\n", Environment.NewLine)); + + // Act + var productAttr = AssemblyInfoFile.GetAttribute("AssemblyProduct", infoFile).Value; + var versionAttr = AssemblyInfoFile.GetAttribute("AssemblyVersion", infoFile).Value; + + // Assert + productAttr.Value.ShouldEqual("\"TestLib\""); + versionAttr.Value.ShouldEqual("\"1.0.0.0\""); + }; } } From 0822521642765d2b5f60bcc84bc2d4d5e9b43ffd Mon Sep 17 00:00:00 2001 From: dsm Date: Wed, 13 Jan 2016 21:29:57 +0100 Subject: [PATCH 2/2] AssemblyInfoFile: add doc strings for read and update functions --- src/app/FakeLib/AssemblyInfoFile.fs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/FakeLib/AssemblyInfoFile.fs b/src/app/FakeLib/AssemblyInfoFile.fs index bb6add5a430..ba8e5060416 100644 --- a/src/app/FakeLib/AssemblyInfoFile.fs +++ b/src/app/FakeLib/AssemblyInfoFile.fs @@ -269,6 +269,9 @@ let private removeAtEnd (textToRemove:string) (text:string) = else text +/// Read attributes from an AssemblyInfo file and return as a sequence of Attribute. +/// ## Parameters +/// - `assemblyInfoFile` - The file to read attributes from. Language C#, F#, VB or C++ is determined from the extension. let GetAttributes assemblyInfoFile = let text = File.ReadAllText assemblyInfoFile @@ -286,9 +289,17 @@ let GetAttributes assemblyInfoFile = m.Groups.["value"].Value, "")) +/// Read a single attribute from an AssemblyInfo file. +/// ## Parameters +/// - `attrName` - Name of the attribute without "Attribute" at the end. +/// - `assemblyInfoFile` - The file to read from. Language C#, F#, VB or C++ is determined from the extension. let GetAttribute attrName assemblyInfoFile = assemblyInfoFile |> GetAttributes |> Seq.tryFind (fun a -> a.Name = attrName) +/// Read the value of a single attribute from an AssemblyInfo file. Note that string values are returned with surrounding "". +/// ## Parameters +/// - `attrName` - Name of the attribute without "Attribute" at the end. +/// - `assemblyInfoFile` - The file to read from. Language C#, F#, VB or C++ is determined from the extension. let GetAttributeValue attrName assemblyInfoFile = match GetAttribute attrName assemblyInfoFile with | Some attr -> Some attr.Value @@ -313,6 +324,10 @@ let private updateAttr regexFactory text (attribute:Attribute) = else failwithf "Attribute '%s' not found" attribute.Name +/// Update a set of attributes in an AssemblyInfo file. Fails if any attribute is not found. +/// ## Parameters +/// - `assemblyInfoFile` - The file to update. Language C#, F#, VB or C++ is determined from the extension. +/// - `attributes` - The Attributes that should be updated matched on Name (Namespace is not used). let UpdateAttributes assemblyInfoFile (attributes: seq) = tracefn "Updating attributes in: %s" assemblyInfoFile