From f7a755332ebdbaaa9f6f05a9d8322b6a2f08bce6 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Wed, 27 Jan 2021 07:53:29 +0000 Subject: [PATCH] Add nullable reference types support. --- src/MapTo/Models.cs | 34 +++++++--- src/MapTo/Sources/ITypeConverterSource.cs | 3 +- src/MapTo/Sources/MapClassSource.cs | 6 +- .../MapTypeConverterAttributeSource.cs | 8 ++- src/MapTo/Sources/SourceBuilder.cs | 24 +++++-- src/MapTo/Sources/SourceCode.cs | 4 -- .../Infrastructure/CSharpGenerator.cs | 14 ++-- test/MapTo.Tests/Tests.cs | 67 +++++++++++++++++++ test/TestConsoleApp/Program.cs | 15 +++-- test/TestConsoleApp/TestConsoleApp.csproj | 8 ++- .../ViewModels/UserViewModel.cs | 2 +- 11 files changed, 146 insertions(+), 39 deletions(-) delete mode 100644 src/MapTo/Sources/SourceCode.cs diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 8442b32..e676f53 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -1,20 +1,11 @@ using System.Collections.Immutable; using MapTo.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace MapTo { - internal record SourceGenerationOptions( - AccessModifier ConstructorAccessModifier, - AccessModifier GeneratedMethodsAccessModifier, - bool GenerateXmlDocument) - { - internal static SourceGenerationOptions From(GeneratorExecutionContext context) => new( - context.GetBuildGlobalOption(nameof(ConstructorAccessModifier)), - context.GetBuildGlobalOption(nameof(GeneratedMethodsAccessModifier)), - context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true) - ); - } + internal record SourceCode(string Text, string HintName); internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray TypeConverterParameters); @@ -28,4 +19,25 @@ namespace MapTo string SourceClassFullName, ImmutableArray MappedProperties ); + + internal record SourceGenerationOptions( + AccessModifier ConstructorAccessModifier, + AccessModifier GeneratedMethodsAccessModifier, + bool GenerateXmlDocument, + bool SupportNullableReferenceTypes) + { + internal static SourceGenerationOptions From(GeneratorExecutionContext context) + { + var compilationOptions = (context.Compilation as CSharpCompilation)?.Options; + + return new( + context.GetBuildGlobalOption(nameof(ConstructorAccessModifier)), + context.GetBuildGlobalOption(nameof(GeneratedMethodsAccessModifier)), + context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true), + compilationOptions is not null && (compilationOptions.NullableContextOptions == NullableContextOptions.Warnings || compilationOptions.NullableContextOptions == NullableContextOptions.Enable) + ); + } + + public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty; + } } \ No newline at end of file diff --git a/src/MapTo/Sources/ITypeConverterSource.cs b/src/MapTo/Sources/ITypeConverterSource.cs index 819e523..b93d9cc 100644 --- a/src/MapTo/Sources/ITypeConverterSource.cs +++ b/src/MapTo/Sources/ITypeConverterSource.cs @@ -14,6 +14,7 @@ namespace MapTo.Sources { using var builder = new SourceBuilder() .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) .WriteLine() .WriteLine($"namespace {RootNamespace}") .WriteOpeningBracket(); @@ -44,7 +45,7 @@ namespace MapTo.Sources } builder - .WriteLine("TDestination Convert(TSource source, object[] converterParameters);") + .WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);") .WriteClosingBracket() .WriteClosingBracket(); diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index b6e036d..d5e3bac 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -9,6 +9,8 @@ namespace MapTo.Sources { using var builder = new SourceBuilder() .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) + .WriteLine() .WriteUsings(model) .WriteLine() @@ -90,7 +92,7 @@ namespace MapTo.Sources return builder .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName} {sourceClassParameterName})") + .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") .WriteOpeningBracket() .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});") .WriteClosingBracket(); @@ -127,7 +129,7 @@ namespace MapTo.Sources return builder .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName} {sourceClassParameterName})") + .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") .WriteOpeningBracket() .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});") .WriteClosingBracket(); diff --git a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs index 663e549..04caf0f 100644 --- a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs +++ b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs @@ -12,9 +12,11 @@ namespace MapTo.Sources internal const string ConverterParametersPropertyName = "ConverterParameters"; internal static SourceCode Generate(SourceGenerationOptions options) - { + { using var builder = new SourceBuilder() .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) + .WriteLine() .WriteLine("using System;") .WriteLine() .WriteLine($"namespace {RootNamespace}") @@ -44,7 +46,7 @@ namespace MapTo.Sources } builder - .WriteLine($"public {AttributeClassName}(Type converter, object[] converterParameters = null)") + .WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)") .WriteOpeningBracket() .WriteLine($"{ConverterPropertyName} = converter;") .WriteLine($"{ConverterParametersPropertyName} = converterParameters;") @@ -72,7 +74,7 @@ namespace MapTo.Sources } builder - .WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}") + .WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}") .WriteClosingBracket() .WriteClosingBracket(); diff --git a/src/MapTo/Sources/SourceBuilder.cs b/src/MapTo/Sources/SourceBuilder.cs index 2777479..984b156 100644 --- a/src/MapTo/Sources/SourceBuilder.cs +++ b/src/MapTo/Sources/SourceBuilder.cs @@ -22,18 +22,32 @@ namespace MapTo.Sources _indentedWriter.Dispose(); } - public SourceBuilder WriteLine(string value) + public SourceBuilder WriteLine(string? value = null) { - _indentedWriter.WriteLine(value); + if (string.IsNullOrWhiteSpace(value)) + { + _indentedWriter.WriteLineNoTabs(string.Empty); + } + else + { + _indentedWriter.WriteLine(value); + } + return this; } - - public SourceBuilder WriteLine() + + public SourceBuilder WriteLineIf(bool condition, string? value) { - _indentedWriter.WriteLineNoTabs(string.Empty); + if (condition) + { + WriteLine(value); + } + return this; } + public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable"); + public SourceBuilder WriteOpeningBracket() { _indentedWriter.WriteLine("{"); diff --git a/src/MapTo/Sources/SourceCode.cs b/src/MapTo/Sources/SourceCode.cs deleted file mode 100644 index c9f8f4b..0000000 --- a/src/MapTo/Sources/SourceCode.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace MapTo.Sources -{ - internal record SourceCode(string Text, string HintName); -} \ No newline at end of file diff --git a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs index 8b390e2..cd6c22c 100644 --- a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs +++ b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs @@ -11,7 +11,7 @@ namespace MapTo.Tests.Infrastructure { internal static class CSharpGenerator { - internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary analyzerConfigOptions = null) + internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary analyzerConfigOptions = null, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable) { var syntaxTree = CSharpSyntaxTree.ParseText(source); var references = AppDomain.CurrentDomain.GetAssemblies() @@ -19,25 +19,29 @@ namespace MapTo.Tests.Infrastructure .Select(a => MetadataReference.CreateFromFile(a.Location)) .ToList(); - var compilation = CSharpCompilation.Create($"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var compilation = CSharpCompilation.Create( + $"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions)); if (assertCompilation) { // NB: fail tests when the injected program isn't valid _before_ running generators compilation.GetDiagnostics().ShouldBeSuccessful(); } - + var driver = CSharpGeneratorDriver.Create( new[] { new MapToGenerator() }, optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions) ); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); - + var diagnostics = outputCompilation.GetDiagnostics() .Where(d => d.Severity >= DiagnosticSeverity.Warning) .Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()} [in \"{c.Location.SourceTree?.FilePath}\"]").ToArray(); - + if (diagnostics.Any()) { Assert.False(diagnostics.Any(), $@"Failed: diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs index bc20428..39aad33 100644 --- a/test/MapTo.Tests/Tests.cs +++ b/test/MapTo.Tests/Tests.cs @@ -286,6 +286,7 @@ namespace Test const string expectedResult = @" // + using System; namespace Test @@ -355,6 +356,7 @@ namespace Test const string expectedResult = @" // + using System; namespace Test @@ -404,6 +406,7 @@ namespace Test const string expectedResult = @" // + using System; namespace Test @@ -514,6 +517,32 @@ namespace MapTo compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); } + [Fact] + public void VerifyTypeConverterInterfaceWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +namespace MapTo +{{ + public interface ITypeConverter + {{ + TDestination Convert(TSource source, object[]? converterParameters); + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); + } + [Fact] public void VerifyMapTypeConverterAttribute() { @@ -521,6 +550,7 @@ namespace MapTo const string source = ""; var expectedInterface = $@" {Constants.GeneratedFilesHeader} + using System; namespace MapTo @@ -548,6 +578,43 @@ namespace MapTo diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); } + + [Fact] + public void VerifyMapTypeConverterAttributeWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class MapTypeConverterAttribute : Attribute + {{ + public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) + {{ + Converter = converter; + ConverterParameters = converterParameters; + }} + + public Type Converter {{ get; }} + + public object[]? ConverterParameters {{ get; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); + } [Fact] public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index be96136..ddffa3b 100644 --- a/test/TestConsoleApp/Program.cs +++ b/test/TestConsoleApp/Program.cs @@ -1,6 +1,13 @@ using TestConsoleApp.ViewModels; -using VM = TestConsoleApp.ViewModels; -using Data = TestConsoleApp.Data.Models; -var userViewModel = VM.User.From(new Data.User()); -var userViewModel2 = new Data.User().ToUserViewModel(); \ No newline at end of file +namespace TestConsoleApp +{ + internal class Program + { + private static void Main(string[] args) + { + var userViewModel = User.From(new Data.Models.User()); + var userViewModel2 = new Data.Models.User().ToUserViewModel(); + } + } +} \ No newline at end of file diff --git a/test/TestConsoleApp/TestConsoleApp.csproj b/test/TestConsoleApp/TestConsoleApp.csproj index 7ce55d3..274cf0d 100644 --- a/test/TestConsoleApp/TestConsoleApp.csproj +++ b/test/TestConsoleApp/TestConsoleApp.csproj @@ -2,14 +2,16 @@ Exe - net5.0 + netcoreapp3.1 + 8 + annotations - + - + Internal diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs index bca6bc9..e32b172 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels private class LastNameConverter : ITypeConverter { /// - public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter"; + public string Convert(long source, object[]? converterParameters) => $"{source} :: With Type Converter"; } } } \ No newline at end of file