diff --git a/.editorconfig b/.editorconfig index 02c38f2..1e10b78 100644 --- a/.editorconfig +++ b/.editorconfig @@ -120,15 +120,15 @@ dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.symbols = static dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.static_readonly_fields.required_modifiers = static,readonly +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * # internal and private fields should be camelCase -dotnet_naming_rule.private_members_with_underscore.symbols = private_fields -dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore dotnet_naming_rule.private_members_with_underscore.severity = suggestion -dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private dotnet_naming_style.prefix_underscore.capitalization = camel_case @@ -159,7 +159,7 @@ dotnet_naming_rule.interfaces_must_start_with_i.style = begin_with_i_style dotnet_naming_symbols.interface_symbols.applicable_kinds = interface dotnet_naming_symbols.interface_symbols.applicable_accessibilities = * -dotnet_naming_style.begin_with_i_style.capitalization = pascal_case +dotnet_naming_style.begin_with_i_style.capitalization = pascal_case dotnet_naming_style.begin_with_i_style.required_prefix = I # name everything else using PascalCase @@ -168,7 +168,7 @@ dotnet_naming_rule.methods_and_properties_must_be_pascal_case.symbols = everythi dotnet_naming_rule.methods_and_properties_must_be_pascal_case.style = pascal_case_style #dotnet_naming_symbols.everything_else_symbols.applicable_kinds = class,struct,enum,delegate,event,method,property,namespace,type_parameter -dotnet_naming_symbols.everything_else_symbols.applicable_kinds = class,struct,enum,delegate,event,method,property +dotnet_naming_symbols.everything_else_symbols.applicable_kinds = class, struct, enum, delegate, event, method, property dotnet_naming_symbols.everything_else_symbols.applicable_accessibilities = * # StyleCop diagnostics severity diff --git a/Directory.Build.props b/Directory.Build.props index 0ed8c53..1fafc8d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,32 +1,32 @@ - - true - true - 1591 - + + true + true + 1591 + - - $(MSBuildThisFileDirectory) - en-US - false - - - git - false - + + $(MSBuildThisFileDirectory) + en-US + false + + + git + false + - - latest - enable - + + latest + enable + - - CODE_ANALYSIS - true - + + CODE_ANALYSIS + true + - - - + + + \ No newline at end of file diff --git a/src/Testura.Code.Tests/Saver/CodeSaverTests.cs b/src/Testura.Code.Tests/Saver/CodeSaverTests.cs index 8707a61..999f731 100644 --- a/src/Testura.Code.Tests/Saver/CodeSaverTests.cs +++ b/src/Testura.Code.Tests/Saver/CodeSaverTests.cs @@ -1,11 +1,15 @@ -using System.Collections.Generic; +namespace Testura.Code.Tests.Saver; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Code.Builders; +using Code.Models.Options; +using Code.Saver; using Microsoft.CodeAnalysis.CSharp.Formatting; using NUnit.Framework; -using Testura.Code.Builders; -using Testura.Code.Models.Options; -using Testura.Code.Saver; - -namespace Testura.Code.Tests.Saver; [TestFixture] public class CodeSaverTests @@ -18,25 +22,80 @@ public void SetUp() _coderSaver = new CodeSaver(); } + [Test] + public async Task SaveCodeToFileAsync_WhenSavingCodeAsFile_ShouldSaveCorrectly() + { + var cts = new CancellationTokenSource(); + var destFile = PrepareDestinationFile(); + var compiledCode = new ClassBuilder("TestClass", "test").Build(); + await _coderSaver.SaveCodeToFileAsync(compiledCode, destFile.FullName, cts.Token); + Assert.IsTrue(File.Exists(destFile.FullName)); + var code = await File.ReadAllTextAsync(destFile.FullName, cts.Token); + Assert.IsNotNull(code); + Assert.AreEqual( + "namespace test\r\n{\r\n public class TestClass\r\n {\r\n }\r\n}", + code); + + FileInfo PrepareDestinationFile() + { + var fi = GetDestinationFile(); + if (fi.Exists) + { + fi.Delete(); + } + + return fi; + } + + // Returns a temporary predictable file name which will be saved to the filesystem for testing. + // TODO: Use a filesystem abstraction library to avoid saving to file system. + FileInfo GetDestinationFile() + { + var exampleFileName = + nameof(SaveCodeToFileAsync_WhenSavingCodeAsFile_ShouldSaveCorrectly); + var destinationFile = Path.Combine( + Environment.CurrentDirectory, + "UnitTests", + "Saver", + $"{exampleFileName}.cs"); + + var fi = new FileInfo(destinationFile); + Directory.CreateDirectory(fi.Directory.FullName); + if (fi.Exists) + { + fi.Delete(); + } + + return fi; + } + } + [Test] public void SaveCodeAsString_WhenSavingCodeAsString_ShouldGetString() { var code = _coderSaver.SaveCodeAsString(new ClassBuilder("TestClass", "test").Build()); Assert.IsNotNull(code); - Assert.AreEqual("namespace test\r\n{\r\n public class TestClass\r\n {\r\n }\r\n}", code); + Assert.AreEqual( + "namespace test\r\n{\r\n public class TestClass\r\n {\r\n }\r\n}", + code); } [Test] public void SaveCodeAsString_WhenSavingCodeAsStringAndOptions_ShouldGetString() { - var codeSaver = new CodeSaver(new List { new OptionKeyValue(CSharpFormattingOptions.NewLinesForBracesInMethods, false) }); + var codeSaver = new CodeSaver( + new List + { + new(CSharpFormattingOptions.NewLinesForBracesInMethods, false) + }); var code = codeSaver.SaveCodeAsString( - new ClassBuilder("TestClass", "test") - .WithMethods( - new MethodBuilder("MyMethod") - .Build()) + new ClassBuilder("TestClass", "test").WithMethods(new MethodBuilder("MyMethod").Build()) .Build()); Assert.IsNotNull(code); - Assert.AreEqual("namespace test\r\n{\r\n public class TestClass\r\n {\r\n void MyMethod() {\r\n }\r\n }\r\n}", code); + Assert.AreEqual( + "namespace test\r\n{\r\n public class TestClass\r\n {\r\n void MyMethod() {\r\n }\r\n }\r\n}", + code); } -} \ No newline at end of file +} + + diff --git a/src/Testura.Code.Tests/Testura.Code.Tests.csproj b/src/Testura.Code.Tests/Testura.Code.Tests.csproj index 033a5de..02f68d8 100644 --- a/src/Testura.Code.Tests/Testura.Code.Tests.csproj +++ b/src/Testura.Code.Tests/Testura.Code.Tests.csproj @@ -8,16 +8,16 @@ - - - - - - + + + + + + - + diff --git a/src/Testura.Code/Builders/Base/BuilderBase.cs b/src/Testura.Code/Builders/Base/BuilderBase.cs index 2c48d26..e9fe04b 100644 --- a/src/Testura.Code/Builders/Base/BuilderBase.cs +++ b/src/Testura.Code/Builders/Base/BuilderBase.cs @@ -1,18 +1,20 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Testura.Code.Builders.BuilderHelpers; -using Testura.Code.Builders.BuildMembers; +namespace Testura.Code.Builders.Base; -namespace Testura.Code.Builders.Base; +using BuilderHelpers; +using BuildMembers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; public abstract class BuilderBase where TBuilder : BuilderBase { + private readonly MemberHelper _memberHelper; private readonly NamespaceHelper _namespaceHelper; private readonly UsingHelper _usingHelper; - private readonly MemberHelper _memberHelper; - protected BuilderBase(string @namespace, NamespaceType namespaceType) + protected BuilderBase( + string @namespace, + NamespaceType namespaceType) { _memberHelper = new MemberHelper(); _usingHelper = new UsingHelper(); @@ -22,24 +24,26 @@ protected BuilderBase(string @namespace, NamespaceType namespaceType) protected bool HaveMembers => _memberHelper.Members.Any(); /// - /// Set the using directives. + /// Set the using directives. /// /// A set of wanted using directive names. /// The current builder public TBuilder WithUsings(params string[] usings) { _usingHelper.AddUsings(usings); + return (TBuilder)this; } /// - /// Add build members that will be generated. + /// Add build members that will be generated. /// /// Build members to add /// The current builder public TBuilder With(params IBuildMember[] buildMembers) { _memberHelper.AddMembers(buildMembers); + return (TBuilder)this; } @@ -48,12 +52,16 @@ protected CompilationUnitSyntax BuildUsings(CompilationUnitSyntax @base) return _usingHelper.BuildUsings(@base); } - protected CompilationUnitSyntax BuildNamespace(CompilationUnitSyntax @base, params MemberDeclarationSyntax[] members) + protected CompilationUnitSyntax BuildNamespace( + CompilationUnitSyntax @base, + params MemberDeclarationSyntax[] members) { return _namespaceHelper.BuildNamespace(@base, members); } - protected CompilationUnitSyntax BuildNamespace(CompilationUnitSyntax @base, SyntaxList members) + protected CompilationUnitSyntax BuildNamespace( + CompilationUnitSyntax @base, + SyntaxList members) { return _namespaceHelper.BuildNamespace(@base, members); } @@ -73,3 +81,4 @@ protected CompilationUnitSyntax BuildMembers(CompilationUnitSyntax compilationUn return _memberHelper.BuildMembers(compilationUnitSyntax); } } + diff --git a/src/Testura.Code/Saver/CodeSaver.cs b/src/Testura.Code/Saver/CodeSaver.cs index 773cb73..8c4ed4b 100644 --- a/src/Testura.Code/Saver/CodeSaver.cs +++ b/src/Testura.Code/Saver/CodeSaver.cs @@ -33,52 +33,100 @@ public CodeSaver(IEnumerable options) /// /// Save generated code to a file. /// - /// Generated code. - /// Full output path. - public void SaveCodeToFile(CompilationUnitSyntax cu, string path) + /// Generated code. + /// Full output destinationFileAbsolutePath. + public void SaveCodeToFile(CompilationUnitSyntax compiledSourceCode, string destinationFileAbsolutePath) { - if (cu == null) + if (compiledSourceCode == null) { - throw new ArgumentNullException(nameof(cu)); + throw new ArgumentNullException(nameof(compiledSourceCode)); } - if (string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(destinationFileAbsolutePath)) { - throw new ArgumentException("Value cannot be null or empty.", nameof(path)); + throw new ArgumentException("Value cannot be null or empty.", nameof(destinationFileAbsolutePath)); } - var workspace = CreateWorkspace(); - var formattedCode = Formatter.Format(cu, workspace); - using var sourceWriter = new StreamWriter(path); + EnsurePathExists(destinationFileAbsolutePath); + using var createdWorkspaceForCodeGen = CreateWorkspace(); + var formattedCode = Formatter.Format(compiledSourceCode, createdWorkspaceForCodeGen); + createdWorkspaceForCodeGen.Dispose(); + using var sourceWriter = new StreamWriter(destinationFileAbsolutePath); formattedCode.WriteTo(sourceWriter); } + /// + /// Save generated code to a file asynchronously + /// + /// Generated code. + /// Full output destinationFileAbsolutePath. + /// The cancellation token. + /// Writes the generated code to the supplied by the user. + public async Task SaveCodeToFileAsync(CompilationUnitSyntax compiledSourceCode, string destinationFileAbsolutePath, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (compiledSourceCode == null) + { + throw new ArgumentNullException(nameof(compiledSourceCode)); + } + + if (string.IsNullOrEmpty(destinationFileAbsolutePath)) + { + throw new ArgumentException("Value cannot be null or empty.", nameof(destinationFileAbsolutePath)); + } + + EnsurePathExists(destinationFileAbsolutePath); + await using var fileStream = File.Open(destinationFileAbsolutePath, FileMode.Create, FileAccess.Write); + await using var sourceWriter = new StreamWriter(fileStream); + using var createdWorkspaceForCodeGen = CreateWorkspace(); + await sourceWriter.WriteAsync(Formatter.Format(compiledSourceCode, createdWorkspaceForCodeGen, cancellationToken: cancellationToken).ToFullString()); + createdWorkspaceForCodeGen.Dispose(); + } + /// /// Save generated code as a string. /// - /// Generated code. + /// Generated code. /// Generated code as a string. - public string SaveCodeAsString(CompilationUnitSyntax cu) + public string SaveCodeAsString(CompilationUnitSyntax compiledSourceCode) { - if (cu == null) + if (compiledSourceCode == null) { - throw new ArgumentNullException(nameof(cu)); + throw new ArgumentNullException(nameof(compiledSourceCode)); } - var workspace = CreateWorkspace(); - var formattedCode = Formatter.Format(cu, workspace); + using var createdWorkspaceForCodeGen = CreateWorkspace(); + var formattedCode = Formatter.Format(compiledSourceCode, createdWorkspaceForCodeGen); + createdWorkspaceForCodeGen.Dispose(); return formattedCode.ToFullString(); } private AdhocWorkspace CreateWorkspace() { - var cw = new AdhocWorkspace(); - cw.Options.WithChangedOption(CSharpFormattingOptions.IndentBraces, true); + var createdWorkspaceForCodeGen = new AdhocWorkspace(); + createdWorkspaceForCodeGen.Options.WithChangedOption(CSharpFormattingOptions.IndentBraces, true); foreach (var optionKeyValue in _options) { - cw.TryApplyChanges(cw.CurrentSolution.WithOptions(cw.Options.WithChangedOption(optionKeyValue.FormattingOption, optionKeyValue.Value))); + createdWorkspaceForCodeGen.TryApplyChanges(createdWorkspaceForCodeGen.CurrentSolution.WithOptions(createdWorkspaceForCodeGen.Options.WithChangedOption(optionKeyValue.FormattingOption, optionKeyValue.Value))); + } + + return createdWorkspaceForCodeGen; + } + + private void EnsurePathExists(string destinationFileAbsolutePath) + { + var fileInfo = new FileInfo(destinationFileAbsolutePath); + if (fileInfo.Directory == null) + { + throw new DirectoryNotFoundException( + $"The parent directory of the target destination file cannot be null. Target destination file full path: { + fileInfo.FullName}"); } - return cw; + if (!Directory.Exists(fileInfo.Directory.FullName)) + { + Directory.CreateDirectory(fileInfo.Directory.FullName); + } } } diff --git a/src/Testura.Code/Saver/ICodeSaver.cs b/src/Testura.Code/Saver/ICodeSaver.cs index a12a7e3..500e117 100644 --- a/src/Testura.Code/Saver/ICodeSaver.cs +++ b/src/Testura.Code/Saver/ICodeSaver.cs @@ -8,14 +8,26 @@ public interface ICodeSaver /// /// Save generated code to a file. /// - /// Generated code. - /// Full output path. - void SaveCodeToFile(CompilationUnitSyntax cu, string path); + /// Generated code. + /// Full output path. + void SaveCodeToFile(CompilationUnitSyntax compiledSourceCode, string destinationFileAbsolutePath); /// /// Save generated code as a string. /// - /// Generated code. + /// Generated code. /// Generated code as a string. - string SaveCodeAsString(CompilationUnitSyntax cu); -} \ No newline at end of file + string SaveCodeAsString(CompilationUnitSyntax compiledSourceCode); + + /// + /// Save generated code to a file. + /// + /// Generated code. + /// Full output path. + /// The cancellation token + /// Saves the generated code to the filesystem. + Task SaveCodeToFileAsync( + CompilationUnitSyntax compiledSourceCode, + string destinationFileAbsolutePath, + CancellationToken cancellationToken = default); +} diff --git a/src/Testura.Code/Settings/Testura.ruleset b/src/Testura.Code/Settings/Testura.ruleset index 26e8d4b..dc1d6a1 100644 --- a/src/Testura.Code/Settings/Testura.ruleset +++ b/src/Testura.Code/Settings/Testura.ruleset @@ -1,91 +1,91 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Testura.Code/Testura.Code.csproj b/src/Testura.Code/Testura.Code.csproj index 79515ac..4b930a6 100644 --- a/src/Testura.Code/Testura.Code.csproj +++ b/src/Testura.Code/Testura.Code.csproj @@ -16,13 +16,14 @@ Testura.Code is a wrapper around the Roslyn API and used for generation, saving and and compiling C# code. It provides methods and helpers to generate classes, methods, statements and expressions. 1.3.0 -- Added support for object initialization (with ObjectCreationGenerator) + - Added support for object initialization (with ObjectCreationGenerator) + Code generation roslyn https://i.ibb.co/nnSPd11/logo128-new.png https://github.com/Testura/Testura.Code - logo.png + logo.png $(RepositoryUrl) true MIT @@ -34,14 +35,14 @@ 3 - - - - - - + + + + + +